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

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

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

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

ติดตั้ง Bazel

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

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

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

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

อย่างไรก็ตาม คุณอาจยังต้องการติดตั้ง Toolchain ของ 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 เพื่อบอกว่าเราต้องการสร้างอะไร โดยปกติแล้วคุณจะเขียนไฟล์เช่นนี้ในแต่ละไดเรกทอรี สำหรับโปรเจ็กต์นี้ เรามีเป้าหมาย go_binary เป้าหมายเดียวที่สร้างโปรแกรมจาก hello.go

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

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

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

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

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

สุดท้าย MODULE.bazel.lock เป็นไฟล์ที่ Bazel สร้างขึ้นซึ่งมีแฮชและข้อมูลเมตาอื่นๆ เกี่ยวกับทรัพยากร Dependency ของเรา โดยรวมถึงทรัพยากร Dependency โดยนัยที่ Bazel เพิ่มเอง ดังนั้นไฟล์นี้จึงค่อนข้างยาวและเราจะไม่แสดงที่นี่ เช่นเดียวกับ go.sum คุณควรคอมมิตไฟล์ MODULE.bazel.lock ไปยังระบบควบคุมเวอร์ชันเพื่อให้ทุกคนในโปรเจ็กต์ได้รับทรัพยากร Dependency แต่ละรายการเวอร์ชันเดียวกัน คุณ ไม่จำเป็นต้องแก้ไข 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++ คุณสามารถดู เอกสารประกอบของกฎเหล่านี้ได้ใน Build Encyclopedia นอกจากนี้ คุณยังดูชุดกฎสำหรับภาษาและเครื่องมืออื่นๆ อีกมากมายได้ใน 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 กำหนดให้เป้าหมายทั้งหมดประกาศทรัพยากร Dependency อย่างชัดเจนด้วยแอตทริบิวต์ต่างๆ เช่น deps ซึ่งอาจดูยุ่งยากเนื่องจากมีการระบุทรัพยากร Dependency ด้วย ในไฟล์ต้นฉบับ แต่ความชัดเจนของ Bazel ทำให้ Bazel มีข้อได้เปรียบ Bazel จะสร้างกราฟการดำเนินการ ที่มีคำสั่ง อินพุต และเอาต์พุตทั้งหมดก่อนที่จะเรียกใช้คำสั่งใดๆ โดยไม่ต้องอ่านไฟล์ต้นฉบับ จากนั้น Bazel จะแคชผลลัพธ์การดำเนินการหรือส่งการดำเนินการสำหรับการดำเนินการระยะไกลได้โดยไม่ต้องมีตรรกะเฉพาะภาษาในตัว

ทำความเข้าใจป้ายกำกับ

ป้ายกำกับคือสตริงที่ Bazel ใช้เพื่อระบุเป้าหมายหรือไฟล์ ป้ายกำกับจะใช้ในอาร์กิวเมนต์บรรทัดคำสั่งและในแอตทริบิวต์ไฟล์ BUILD เช่น deps เราได้เห็นป้ายกำกับบางรายการแล้ว เช่น //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 rules_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.

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

$ bazel test //...

สรุปและอ่านเพิ่มเติม

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

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