本教程将通过介绍如何构建 Go (Golang) 项目,向您介绍 Bazel 的基础知识。您将学习如何设置工作区、构建小程序、导入库以及运行其测试。在此过程中,您将学习一些重要的 Bazel 概念,例如目标和 BUILD
文件。
预计完成时间:30 分钟
准备工作
安装 Bazel
在开始之前,如果您尚未安装 bazel,请先执行此操作。
您可以在任何目录中运行 bazel version
来检查 Bazel 是否已安装。
安装 Go(可选)
您无需安装 Go 即可使用 Bazel 构建 Go 项目。Bazel Go 规则集会自动下载并使用 Go 工具链,而不是使用您机器上安装的工具链。这样可以确保项目中的所有开发者使用相同版本的 Go 进行构建。
不过,您可能仍需要安装 Go 工具链,以运行 go
get
和 go mod tidy
等命令。
您可以在任何目录中运行 go version
来检查是否已安装 Go。
获取示例项目
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.mod
文件可能仍然很有用,这样您就可以继续使用 go get
和 go mod tidy
进行依赖项管理。Bazel Go 规则集可以从 go.mod
导入依赖项,但我们将在另一个教程中介绍这一点。
我们的 MODULE.bazel
文件包含对 Go 规则集 rules_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++ 等几种语言提供了内置规则。您可以在构建百科全书中找到这些规则的文档。您可以在 Bazel 中央注册库 (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
软件包相关,该软件包用于访问复制到可执行文件中的数据文件。这是一个不幸的名称冲突: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 概念。