Bazel 教學課程:建構 Go 專案

回報問題 查看來源 Nightly · 8.4 · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

本教學課程將說明如何建構 Go (Golang) 專案,藉此介紹 Bazel 的基本概念。您將瞭解如何設定工作區、建構小型程式、匯入程式庫,以及執行測試。過程中,您將瞭解目標和 BUILD 檔案等重要 Bazel 概念。

預計完成時間:30 分鐘

事前準備

安裝 Bazel

開始之前,請先安裝 bazel (如果您尚未安裝)。

您可以在任何目錄中執行 bazel version,檢查是否已安裝 Bazel。

安裝 Go (選用)

您不需要安裝 Go,就能使用 Bazel 建構 Go 專案。Bazel Go 規則集會自動下載並使用 Go 工具鍊,而非使用安裝在電腦上的工具鍊。這可確保專案中的所有開發人員都使用相同版本的 Go 建構。

不過,您可能仍想安裝 Go 工具鍊,以便執行 go getgo mod tidy 等指令。

如要檢查 Go 是否已安裝,請在任何目錄中執行 go version

取得範例專案

Bazel 範例儲存在 Git 存放區中,因此如果尚未安裝 Git,請先完成這項作業。如要下載範例存放區,請執行下列指令:

git clone https://github.com/bazelbuild/examples

本教學課程的範例專案位於 examples/go-tutorial 目錄中。查看內容:

go-tutorial/
└── stage1
└── stage2
└── stage3

有三個子目錄 (stage1stage2stage3),分別對應本教學課程的不同部分。每個階段都以前一個階段為基礎。

使用 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 getgo 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 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 會使用 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 套件是一組檔案和目標,由頂層目錄中的 BUILDBUILD.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.gofortune_test.go,因此我們在此使用 embed 屬性,將 fortune 目標的屬性併入 fortune_testembed 最常與 go_testgo_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 概念。

  • 如要開始使用 Bazel 建構其他應用程式,請參閱 C++JavaAndroidiOS 的教學課程。
  • 您也可以查看其他語言的建議規則清單。
  • 如要進一步瞭解 Go,請參閱 rules_go 模組,尤其是核心 Go 規則說明文件。
  • 如要進一步瞭解如何在專案外部使用 Bazel 模組,請參閱「外部依附元件」。如要瞭解如何透過 Bazel 的模組系統依附於 Go 模組和工具鍊,請參閱「使用 bzlmod 搭配 Go」。