บทแนะนำของ Bazel: สร้างโครงการ Go

รายงานปัญหา ดูซอร์ส รุ่น Nightly · 8.2 · 8.1 · 8.0 · 7.6 · 7.5

บทแนะนำนี้จะอธิบายข้อมูลเบื้องต้นเกี่ยวกับ Bazel โดยแสดงวิธีสร้างโปรเจ็กต์ Go (Golang) คุณจะได้เรียนรู้วิธีตั้งค่าเวิร์กスペース สร้างโปรแกรมขนาดเล็ก นําเข้าไลบรารี และเรียกใช้การทดสอบ ในระหว่างนี้ คุณจะได้เรียนรู้แนวคิดหลักของ Bazel เช่น เป้าหมายและไฟล์ BUILD

เวลาโดยประมาณที่ใช้ในการดำเนินการ: 30 นาที

ก่อนเริ่มต้น

ติดตั้ง Bazel

ก่อนเริ่มต้น ให้ติดตั้ง Bazel ก่อน หากยังไม่ได้ทำ

คุณสามารถตรวจสอบว่ามีการติดตั้ง Bazel หรือไม่โดยเรียกใช้ bazel version ในไดเรกทอรีใดก็ได้

ติดตั้ง Go (ไม่บังคับ)

คุณไม่จําเป็นต้องติดตั้ง Go เพื่อสร้างโปรเจ็กต์ Go ด้วย Bazel ชุดกฎ Bazel Go จะดาวน์โหลดและใช้ชุดเครื่องมือ Go โดยอัตโนมัติแทนการใช้ชุดเครื่องมือที่ติดตั้งในเครื่อง วิธีนี้ช่วยให้มั่นใจว่านักพัฒนาซอฟต์แวร์ทุกคนในโปรเจ็กต์จะสร้างด้วย Go เวอร์ชันเดียวกัน

อย่างไรก็ตาม คุณอาจยังต้องติดตั้งชุดเครื่องมือ Go เพื่อเรียกใช้คําสั่งอย่าง go get และ go mod tidy

คุณสามารถตรวจสอบว่ามีการติดตั้ง Go หรือไม่โดยเรียกใช้ go version ในไดเรกทอรีใดก็ได้

รับโปรเจ็กต์ตัวอย่าง

ตัวอย่าง Bazel เก็บอยู่ในที่เก็บ Git คุณจึงต้องติดตั้งGit หากยังไม่ได้ดำเนินการ หากต้องการดาวน์โหลดที่เก็บตัวอย่าง ให้เรียกใช้คําสั่งนี้

git clone https://github.com/bazelbuild/examples

โปรเจ็กต์ตัวอย่างสำหรับบทแนะนำนี้อยู่ในไดเรกทอรี examples/go-tutorial ดูสิ่งที่มีให้

go-tutorial/
└── stage1
└── stage2
└── stage3

มีไดเรกทอรีย่อย 3 รายการ (stage1, stage2 และ stage3) แต่ละรายการมีไว้สำหรับส่วนต่างๆ ของบทแนะนำนี้ แต่ละระยะจะต่อยอดจากระยะก่อนหน้า

บิลด์ด้วย Bazel

เริ่มต้นในไดเรกทอรี stage1 ซึ่งเราจะพบโปรแกรม เราสามารถสร้างด้วย bazel build แล้วเรียกใช้ได้โดยทำดังนี้

$ cd go-tutorial/stage1/
$ bazel build //:hello
INFO: Analyzed target //:hello (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //:hello up-to-date:
  bazel-bin/hello_/hello
INFO: Elapsed time: 0.473s, Critical Path: 0.25s
INFO: 3 processes: 1 internal, 2 darwin-sandbox.
INFO: Build completed successfully, 3 total actions

$ bazel-bin/hello_/hello
Hello, Bazel! 💚

นอกจากนี้ เรายังสร้างและเรียกใช้โปรแกรมด้วยคำสั่ง bazel run เดียวได้อีกด้วย โดยทำดังนี้

$ bazel run //:hello
bazel run //:hello
INFO: Analyzed target //:hello (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //:hello up-to-date:
  bazel-bin/hello_/hello
INFO: Elapsed time: 0.128s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/hello_/hello
Hello, Bazel! 💚

ทำความเข้าใจโครงสร้างโปรเจ็กต์

ลองดูโปรเจ็กต์ที่เราเพิ่งสร้าง

hello.go มีซอร์สโค้ด Go สำหรับโปรแกรม

package main

import "fmt"

func main() {
    fmt.Println("Hello, Bazel! 💚")
}

BUILD มีคำสั่งบางอย่างสำหรับ Bazel ซึ่งบอกให้ Bazel รู้ว่าเราต้องการจะสร้างอะไร โดยปกติแล้วคุณจะเขียนไฟล์แบบนี้ในแต่ละไดเรกทอรี สําหรับโปรเจ็กต์นี้ เราใช้เป้าหมาย go_binary รายการเดียวที่สร้างโปรแกรมจาก hello.go

load("@rules_go//go:def.bzl", "go_binary")

go_binary(
    name = "hello",
    srcs = ["hello.go"],
)

MODULE.bazel จะติดตามทรัพยากรของโปรเจ็กต์ นอกจากนี้ ยังทำเครื่องหมายไดเรกทอรีรูทของโปรเจ็กต์ด้วย คุณจึงจะเขียนไฟล์ MODULE.bazel ได้เพียงไฟล์เดียวต่อโปรเจ็กต์ ไฟล์นี้มีวัตถุประสงค์คล้ายกับไฟล์ go.mod ของ Go คุณไม่จำเป็นต้องมีไฟล์ go.mod ในโปรเจ็กต์ Bazel แต่การมีไฟล์ดังกล่าวอาจมีประโยชน์เพื่อให้คุณใช้ go get และ go mod tidy ในการจัดการทรัพยากรต่อไปได้ ชุดกฎ Bazel Go สามารถนําเข้าข้อมูลที่ต้องพึ่งพาจาก go.mod ได้ แต่เราจะอธิบายเรื่องนี้ในบทแนะนําอื่น

ไฟล์ MODULE.bazel ของเรามี 1 รายการที่ต้องพึ่งพา นั่นคือ rules_go ซึ่งเป็นชุดกฎ Go เราต้องใช้ข้อกําหนดนี้เนื่องจาก Bazel ไม่มีการสนับสนุน Go ในตัว

bazel_dep(
    name = "rules_go",
    version = "0.50.1",
)

สุดท้าย MODULE.bazel.lock คือไฟล์ที่ Bazel สร้างขึ้นซึ่งมีแฮชและข้อมูลเมตาอื่นๆ เกี่ยวกับ Dependency ซึ่งจะรวมข้อกําหนดเบื้องต้นที่ Bazel เพิ่มไว้ด้วย จึงค่อนข้างยาวและเราจะไม่แสดงที่นี่ เช่นเดียวกับ go.sum คุณควรคอมมิตไฟล์ MODULE.bazel.lock ไปยังระบบควบคุมแหล่งที่มาเพื่อให้มั่นใจว่าทุกคนในโปรเจ็กต์จะใช้ทรัพยากรแต่ละรายการในเวอร์ชันเดียวกัน คุณไม่จำเป็นต้องแก้ไข MODULE.bazel.lock ด้วยตนเอง

ทำความเข้าใจไฟล์ BUILD

การโต้ตอบกับ Bazel ส่วนใหญ่จะผ่านไฟล์ BUILD (หรือไฟล์ BUILD.bazel) คุณจึงควรทำความเข้าใจสิ่งที่ไฟล์เหล่านี้ทํา

ไฟล์ BUILD เขียนด้วยภาษาสคริปต์ที่เรียกว่า Starlark ซึ่งเป็นชุดย่อยของ Python

ไฟล์ BUILD มีรายการเป้าหมาย เป้าหมายคือสิ่งที่ Bazel สามารถสร้างได้ เช่น ไฟล์ไบนารี ไลบรารี หรือทดสอบ

เป้าหมายจะเรียกฟังก์ชันกฎพร้อมรายการแอตทริบิวต์เพื่ออธิบายสิ่งที่ควรสร้าง ตัวอย่างของเรามีแอตทริบิวต์ 2 รายการ ได้แก่ name ที่ระบุเป้าหมายในบรรทัดคำสั่ง และ srcs คือรายการเส้นทางไฟล์ต้นทาง (คั่นด้วยเครื่องหมายทับ โดยสัมพันธ์กับไดเรกทอรีที่มีไฟล์ BUILD)

กฎจะบอก Bazel ว่าจะสร้างเป้าหมายอย่างไร ในตัวอย่างนี้ เราใช้กฎ go_binary กฎแต่ละข้อจะกำหนดการดำเนินการ (คําสั่ง) ที่สร้างชุดไฟล์เอาต์พุต เช่น go_binary กำหนดการดำเนินการคอมไพล์และลิงก์ของ Go ที่จะสร้างไฟล์เอาต์พุตที่เรียกใช้งานได้

Bazel มีกฎในตัวสำหรับภาษาต่างๆ เช่น Java และ C++ คุณสามารถดูเอกสารประกอบในสารานุกรมการสร้าง คุณดูชุดกฎสำหรับภาษาและเครื่องมืออื่นๆ อีกมากมายได้ใน Bazel Central Registry (BCR)

เพิ่มคลัง

ไปที่ไดเรกทอรี stage2 ซึ่งเราจะสร้างโปรแกรมใหม่ที่พิมพ์คำทำนายของคุณ โปรแกรมนี้ใช้แพ็กเกจ Go แยกต่างหากเป็นไลบรารีที่จะเลือกคำทำนายจากรายการข้อความที่กำหนดไว้ล่วงหน้า

go-tutorial/stage2
├── BUILD
├── MODULE.bazel
├── MODULE.bazel.lock
├── fortune
│   ├── BUILD
│   └── fortune.go
└── print_fortune.go

fortune.go คือไฟล์ต้นฉบับของไลบรารี ไลบรารี fortune เป็นแพ็กเกจ Go แยกต่างหาก ดังนั้นไฟล์ต้นฉบับของไลบรารีจึงอยู่ในไดเรกทอรีแยกต่างหาก Bazel ไม่ได้กำหนดให้คุณเก็บแพ็กเกจ Go ไว้ในไดเรกทอรีแยกต่างหาก แต่นี่เป็นแนวทางปฏิบัติที่ชัดเจนในระบบนิเวศของ Go และการทำตามแนวทางนี้จะช่วยให้คุณใช้งานร่วมกับเครื่องมืออื่นๆ ของ Go ได้

package fortune

import "math/rand"

var fortunes = []string{
    "Your build will complete quickly.",
    "Your dependencies will be free of bugs.",
    "Your tests will pass.",
}

func Get() string {
    return fortunes[rand.Intn(len(fortunes))]
}

ไดเรกทอรี fortune มีไฟล์ BUILD ของตัวเองซึ่งบอก Bazel ว่าจะสร้างแพ็กเกจนี้อย่างไร เราใช้ go_library แทน go_binary

นอกจากนี้ เรายังต้องตั้งค่าแอตทริบิวต์ importpath เป็นสตริงที่นําเข้าไลบรารีไปยังไฟล์ซอร์สโค้ด Go อื่นๆ ได้ ชื่อนี้ควรเป็นเส้นทางที่เก็บ (หรือเส้นทางโมดูล) ที่ต่อเชื่อมกับไดเรกทอรีภายในที่เก็บ

สุดท้าย เราต้องตั้งค่าแอตทริบิวต์ visibility เป็น ["//visibility:public"] visibility สามารถตั้งค่าในเป้าหมายใดก็ได้ ซึ่งจะกำหนดว่าแพ็กเกจ Bazel ใดบ้างที่อาจใช้เป้าหมายนี้ ในกรณีของเรา เราต้องการให้เป้าหมายใดก็ตามใช้คลังนี้ได้ เราจึงใช้ค่าพิเศษ //visibility:public

load("@rules_go//go:def.bzl", "go_library")

go_library(
    name = "fortune",
    srcs = ["fortune.go"],
    importpath = "github.com/bazelbuild/examples/go-tutorial/stage2/fortune",
    visibility = ["//visibility:public"],
)

คุณสร้างคลังนี้ได้โดยใช้สิ่งต่อไปนี้

$ bazel build //fortune

ต่อไป ให้ดูว่า print_fortune.go ใช้แพ็กเกจนี้อย่างไร

package main

import (
    "fmt"

    "github.com/bazelbuild/examples/go-tutorial/stage2/fortune"
)

func main() {
    fmt.Println(fortune.Get())
}

print_fortune.go จะนำเข้าแพ็กเกจโดยใช้สตริงเดียวกันกับที่ประกาศไว้ในแอตทริบิวต์ importpath ของไลบรารี fortune

นอกจากนี้ เรายังต้องประกาศทรัพยากร Dependency นี้กับ Bazel ด้วย นี่คือไฟล์ BUILD ในไดเรกทอรี stage2

load("@rules_go//go:def.bzl", "go_binary")

go_binary(
    name = "print_fortune",
    srcs = ["print_fortune.go"],
    deps = ["//fortune"],
)

ซึ่งจะเรียกใช้ได้โดยใช้คําสั่งด้านล่าง

bazel run //:print_fortune

เป้าหมาย print_fortune มีแอตทริบิวต์ deps ซึ่งเป็นรายการเป้าหมายอื่นๆ ที่เป้าหมายนั้นใช้ ซึ่งมี "//fortune" ซึ่งเป็นสตริงป้ายกำกับที่อ้างอิงถึงเป้าหมายในไดเรกทอรี fortune ชื่อ fortune

Bazel กําหนดให้เป้าหมายทั้งหมดประกาศการพึ่งพาอย่างชัดแจ้งด้วยแอตทริบิวต์ เช่น deps การดำเนินการนี้อาจดูยุ่งยากเนื่องจากมีการระบุการพึ่งพาด้วยในไฟล์ต้นฉบับ แต่ความชัดเจนของ Bazel ช่วยให้มีข้อได้เปรียบ Bazel จะสร้างกราฟการดำเนินการที่มีคำสั่ง อินพุต และเอาต์พุตทั้งหมดก่อนที่จะเรียกใช้คำสั่งใดๆ โดยไม่มีการอ่านไฟล์ต้นทาง จากนั้น Bazel จะแคชผลลัพธ์การดำเนินการหรือส่งการดำเนินการสำหรับการดำเนินการจากระยะไกลได้โดยไม่ต้องมีตรรกะเฉพาะภาษาในตัว

การทำความเข้าใจเรื่องป้ายกำกับ

ป้ายกำกับคือสตริงที่ Bazel ใช้เพื่อระบุเป้าหมายหรือไฟล์ ป้ายกำกับใช้ในอาร์กิวเมนต์บรรทัดคำสั่งและในแอตทริบิวต์ไฟล์ BUILD เช่น deps เราเห็นแล้ว 2-3 รายการ เช่น //fortune, //:print-fortune และ @rules_go//go:def.bzl

ป้ายกำกับประกอบด้วย 3 ส่วน ได้แก่ ชื่อที่เก็บ ชื่อแพ็กเกจ และชื่อเป้าหมาย (หรือไฟล์)

ชื่อที่เก็บจะเขียนอยู่ระหว่าง @ และ // และใช้เพื่ออ้างอิงเป้าหมายจากโมดูล Bazel อื่น (บางครั้งโมดูลและที่เก็บใช้แทนกันได้เนื่องจากเหตุผลทางประวัติศาสตร์) ในป้ายกำกับ @rules_go//go:def.bzl ชื่อที่เก็บคือ rules_go คุณละเว้นชื่อที่เก็บได้เมื่ออ้างอิงถึงเป้าหมายในที่เก็บเดียวกัน

ชื่อแพ็กเกจจะเขียนอยู่ระหว่าง // และ : และใช้เพื่ออ้างอิงเป้าหมายจากแพ็กเกจ Bazel อื่น ในป้ายกํากับ @rules_go//go:def.bzl ชื่อแพ็กเกจคือ go แพ็กเกจ Bazel คือชุดไฟล์และเป้าหมายที่กําหนดโดยไฟล์ BUILD หรือ BUILD.bazel ในไดเรกทอรีระดับบนสุด ชื่อแพ็กเกจคือเส้นทางที่คั่นด้วยเครื่องหมายทับจากไดเรกทอรีรูทของโมดูล (ที่มี MODULE.bazel) ไปยังไดเรกทอรีที่มีไฟล์ BUILD แพ็กเกจอาจมีไดเรกทอรีย่อยได้เฉพาะในกรณีที่ไม่มีไฟล์ BUILD ที่กําหนดแพ็กเกจของตนเอง

โปรเจ็กต์ Go ส่วนใหญ่จะมีไฟล์ BUILD 1 ไฟล์ต่อไดเรกทอรีและแพ็กเกจ Go 1 แพ็กเกจต่อไฟล์ BUILD คุณอาจละชื่อแพ็กเกจในป้ายกำกับได้เมื่ออ้างอิงถึงเป้าหมายในไดเรกทอรีเดียวกัน

ชื่อเป้าหมายจะเขียนต่อจาก : และหมายถึงเป้าหมายภายในแพ็กเกจ คุณอาจไม่ต้องระบุชื่อเป้าหมายหากชื่อนั้นเหมือนกับคอมโพเนนต์สุดท้ายของชื่อแพ็กเกจ (ดังนั้น //a/b/c:c จึงเหมือนกับ //a/b/c และ //fortune:fortune ก็เหมือนกับ //fortune)

ในบรรทัดคำสั่ง คุณสามารถใช้ ... เป็นไวลด์การ์ดเพื่ออ้างอิงเป้าหมายทั้งหมดในแพ็กเกจได้ ซึ่งจะเป็นประโยชน์ต่อการสร้างหรือทดสอบเป้าหมายทั้งหมดในที่เก็บ

# Build everything
$ bazel build //...

ทดสอบโปรเจ็กต์

ถัดไป ให้ไปที่ไดเรกทอรี stage3 ซึ่งเราจะเพิ่มการทดสอบ

go-tutorial/stage3
├── BUILD
├── MODULE.bazel
├── MODULE.bazel.lock
├── fortune
│   ├── BUILD
│   ├── fortune.go
│   └── fortune_test.go
└── print-fortune.go

fortune/fortune_test.go คือไฟล์ต้นทางทดสอบใหม่

package fortune

import (
    "slices"
    "testing"
)

// TestGet checks that Get returns one of the strings from fortunes.
func TestGet(t *testing.T) {
    msg := Get()
    if i := slices.Index(fortunes, msg); i < 0 {
        t.Errorf("Get returned %q, not one the expected messages", msg)
    }
}

ไฟล์นี้ใช้ตัวแปร fortunes ที่ไม่ได้ส่งออก จึงต้องคอมไพล์เป็นแพ็กเกจ Go เดียวกับ fortune.go ดูที่ไฟล์ BUILD เพื่อดูวิธีทํางาน

load("@rules_go//go:def.bzl", "go_library", "go_test")

go_library(
    name = "fortune",
    srcs = ["fortune.go"],
    importpath = "github.com/bazelbuild/examples/go-tutorial/stage3/fortune",
    visibility = ["//visibility:public"],
)

go_test(
    name = "fortune_test",
    srcs = ["fortune_test.go"],
    embed = [":fortune"],
)

เรามีเป้าหมาย fortune_test ใหม่ที่ใช้กฎ go_test เพื่อคอมไพล์และลิงก์ไฟล์ปฏิบัติการทดสอบ go_test ต้องคอมไพล์ fortune.go และ fortune_test.go โดยใช้คําสั่งเดียวกัน เราจึงใช้แอตทริบิวต์ embed ที่นี่เพื่อรวมแอตทริบิวต์ของเป้าหมาย fortune ไว้ใน fortune_test embed มักใช้กับ go_test และ go_binary แต่ใช้กับ go_library ได้ด้วย ซึ่งบางครั้งจะมีประโยชน์สําหรับโค้ดที่สร้างขึ้น

คุณอาจสงสัยว่าแอตทริบิวต์ embed เกี่ยวข้องกับแพ็กเกจ embed ของ Go หรือไม่ ซึ่งใช้เข้าถึงไฟล์ข้อมูลที่คัดลอกไปยังไฟล์ที่เรียกใช้งานได้ นี่เป็นปัญหาการชนกันของชื่อที่ไม่คาดคิด เนื่องจากแอตทริบิวต์ embed ของ rules_go เปิดตัวก่อนแพ็กเกจ embed ของ Go แต่จะใช้ embedsrcs เพื่อแสดงรายการไฟล์ที่โหลดได้ด้วยแพ็กเกจ embed

ลองทำการทดสอบกับ bazel test

$ bazel test //fortune:fortune_test
INFO: Analyzed target //fortune:fortune_test (0 packages loaded, 0 targets configured).
INFO: Found 1 test target...
Target //fortune:fortune_test up-to-date:
  bazel-bin/fortune/fortune_test_/fortune_test
INFO: Elapsed time: 0.168s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
//fortune:fortune_test                                          PASSED in 0.3s

Executed 0 out of 1 test: 1 test passes.
There were tests whose specified size is too big. Use the --test_verbose_timeout_warnings command line option to see which ones these are.

คุณสามารถใช้ไวลด์การ์ด ... เพื่อเรียกใช้การทดสอบทั้งหมดได้ นอกจากนี้ Bazel จะสร้างเป้าหมายที่ไม่ใช่การทดสอบด้วย ซึ่งจะช่วยจับข้อผิดพลาดในการคอมไพล์ได้แม้ในแพ็กเกจที่ไม่มีการทดสอบ

$ bazel test //...

สรุปและแหล่งข้อมูลเพิ่มเติม

ในบทแนะนํานี้ เราได้สร้างและทดสอบโปรเจ็กต์ Go ขนาดเล็กด้วย Bazel และเรียนรู้แนวคิดหลักๆ ของ Bazel ไปพร้อมๆ กัน

  • หากต้องการเริ่มต้นสร้างแอปพลิเคชันอื่นๆ ด้วย Bazel โปรดดูบทแนะนำสำหรับ C++, Java, Android และ iOS
  • นอกจากนี้ คุณยังดูรายการกฎที่แนะนำสำหรับภาษาอื่นๆ ได้ด้วย
  • ดูข้อมูลเพิ่มเติมเกี่ยวกับ Go ได้ที่ข้อบังคับของข้อบังคับ rules_go โดยเฉพาะเอกสารประกอบกฎ Go หลัก
  • ดูข้อมูลเพิ่มเติมเกี่ยวกับการใช้งานโมดูล Bazel นอกโปรเจ็กต์ได้ที่ทรัพยากรภายนอก โดยเฉพาะอย่างยิ่ง ดูข้อมูลเกี่ยวกับวิธีใช้โมดูล Go และชุดเครื่องมือผ่านระบบโมดูลของ Bazel ได้ที่ Go with bzlmod