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

รายงานปัญหา ดูแหล่งที่มา Nightly · 8.4 · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

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

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

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

ติดตั้ง Bazel

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

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

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

คุณไม่จำเป็นต้องติดตั้ง Go เพื่อสร้างโปรเจ็กต์ Go ด้วย Bazel ชุดกฎ Bazel Go จะดาวน์โหลดและใช้ Toolchain Go โดยอัตโนมัติแทนการใช้ Toolchain ที่ติดตั้งในเครื่อง ซึ่งจะช่วยให้มั่นใจได้ว่า นักพัฒนาซอฟต์แวร์ทุกคนในโปรเจ็กต์จะสร้างด้วย 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 ซึ่งบอกว่าเราต้องการสร้างอะไร โดยปกติแล้วคุณจะเขียนไฟล์ลักษณะนี้ในแต่ละไดเรกทอรี สำหรับโปรเจ็กต์นี้ เรามีเป้าหมาย 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 ไฟล์ของเรามีการอ้างอิงเดียวใน 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 ไปยังการควบคุมแหล่งที่มาเพื่อ ให้ทุกคนในโปรเจ็กต์ได้รับทรัพยากร Dependency แต่ละรายการในเวอร์ชันเดียวกัน คุณไม่จำเป็นต้องแก้ไข MODULE.bazel.lock ด้วยตนเอง

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

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

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

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

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

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

Bazel มีกฎในตัวสำหรับภาษาต่างๆ เช่น Java และ C++ คุณสามารถดูเอกสารประกอบใน Build Encyclopedia คุณดูชุดกฎสำหรับภาษาและเครื่องมืออื่นๆ อีกมากมายได้ในรีจิสทรีกลางของ Bazel (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 เราเห็นมาบ้างแล้ว เช่น //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 หนึ่งไฟล์ต่อไดเรกทอรี และแพ็กเกจ Go หนึ่งแพ็กเกจต่อไฟล์ 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 ก่อนแพ็กเกจ 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.
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