Bazel 教學課程:建構 Go 專案

回報問題 查看原始碼 Nightly · 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

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

預計完成時間: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 指令,可告知 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 百科全書中找到相關說明文件。您可以在 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.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.gofortune_test.go,因此我們在此使用 embed 屬性,將 fortune 目標的屬性整合至 fortune_testembed 最常用於 go_testgo_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 核心概念。

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