本教學課程將介紹 Bazel 的基本概念,並說明如何建構 Go (Golang) 專案。您將瞭解如何設定工作區、建構小型程式、匯入程式庫,以及執行測試。您將在過程中瞭解 Bazel 的重要概念,例如目標和 BUILD
檔案。
預計完成時間: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 指令,可告知 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 = "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 百科全書中找到相關說明文件。您可以在 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
會使用 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.Index(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
套件相關,該套件用於存取複製到執行檔中的資料檔案。這是名稱衝突:Rule_go 的 embed
屬性是在 Go 的 embed
套件之前導入。相反地,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 //...
結論和延伸閱讀
在本教學課程中,我們使用 Bazel 建構及測試一個小型 Go 專案,並學習了一些 Bazel 核心概念。