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

รายงานปัญหา ดูแหล่งที่มา รุ่น Nightly · 7.4

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

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

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

สุดท้าย MODULE.bazel.lock คือไฟล์ที่ Bazel สร้างขึ้นซึ่งมีแฮชและข้อมูลเมตาอื่นๆ เกี่ยวกับสิ่งที่เราพึ่งพา ซึ่งจะรวมข้อกําหนดเบื้องต้นที่ 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 compile และลิงก์ที่สร้างไฟล์เอาต์พุตที่เรียกใช้ได้

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 กำหนดให้เป้าหมายทั้งหมดประกาศทรัพยากร Dependency อย่างชัดเจนด้วยแอตทริบิวต์ เช่น 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