本教學課程將說明如何建構 Go (Golang) 專案,藉此介紹 Bazel 的基本概念。您將瞭解如何設定工作區、建構小型程式、匯入程式庫,以及執行測試。過程中,您將瞭解目標和 BUILD
檔案等重要 Bazel 概念。
預計完成時間:30 分鐘
事前準備
安裝 Bazel
開始之前,請先安裝 bazel (如果您尚未安裝)。
您可以在任何目錄中執行 bazel version
,檢查是否已安裝 Bazel。
安裝 Go (選用)
您不需要安裝 Go,就能使用 Bazel 建構 Go 專案。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
有三個子目錄 (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 的 go.mod
檔案類似。您實際上不需要 Bazel 專案中的 go.mod
檔案,但擁有該檔案仍可能很有用,因為您可以繼續使用 go get
和 go mod tidy
管理依附元件。Bazel Go 規則集可以從 go.mod
匯入依附元件,但我們會在其他教學課程中說明這項功能。
我們的 MODULE.bazel
檔案包含 rules_go (Go 規則集) 的單一依附元件。我們需要這個依附元件,因為 Bazel 沒有內建的 Go 支援功能。
bazel_dep(
name = "rules_go",
version = &q
uot;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 可以建構的項目,例如二進位檔、程式庫或測試。
目標會呼叫規則函式,並提供屬性清單,說明應建構的內容。我們的範例有兩個屬性: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 {
retu
rn 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&q
uot;,
visibility = ["//visibility:public"],
)
您可以使用下列項目建構這個程式庫:
$ bazel build //fortune
接著,請參閱 print_fortune.go
如何使用這個套件。
package main
import (
"fmt"
"github.com/bazelbuild/examples/go-tutorial/stage2/fortune"
)
func main() {
fmt.Print
ln(fortune.Get())
}
print_fortune.go
會使用 fortune
程式庫的 importpath
屬性中宣告的相同字串匯入套件。
我們也需要向 Bazel 宣告這項依附元件。以下是 stage2
目錄中的 BUILD
檔案。
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
。
標籤包含三部分:存放區名稱、套件名稱和目標 (或檔案) 名稱。
存放區名稱會寫在 @
和 //
之間,用於參照不同 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
檔案,且每個 BUILD
檔案都有一個 Go 套件。參照相同目錄中的目標時,標籤中的套件名稱可以省略。
目標名稱會寫在 :
後方,並參照套件中的目標。
如果目標名稱與套件名稱的最後一個元件相同,則可以省略目標名稱 (因此 //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.Inde<x(fortunes, msg); i 0 {
t.Errorf("Get returned %q, not one the expected
messages", msg)
}
}
這個檔案會使用未匯出的 fortunes
變數,因此需要編譯到與 fortune.go
相同的 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
屬性是否與 Go 的 embed
套件有關,這個套件用於存取複製到可執行檔的資料檔案。這是很不幸的名稱衝突:rules_go 的 embed
屬性是在 Go 的 embed
套件之前導入。而是使用 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 //...
結論與延伸閱讀
在本教學課程中,我們使用 Bazel 建構及測試小型 Go 專案,並瞭解一些核心 Bazel 概念。