用 Bazel 组织 Go 目标、依赖与测试
原作者:Bazel 文档贡献者。来源:Bazel Tutorial: Build a Go Project。教程通过一个可执行文件、一个库和一个 Go 测试介绍 Bazel 的工作区、MODULE.bazel、BUILD 文件、目标、标签及依赖声明。原文的样例规则版本是 rules_go 0.50.1;本文保留此版本,不能假定它总是最新。

预计阅读时间约 30 分钟。教程项目来自 Bazel examples 仓库中的 go-tutorial,分为 stage1、stage2 与 stage3。每一阶段都各自有一个 MODULE.bazel,代码示例逐步展示项目结构,而不是一个需要合并的单目录工程。
准备工具和样例仓库
先安装 Bazel;可用 bazel version 查看已安装版本。教程的 Go 构建规则可以自动下载并使用 Go 工具链,所以安装 Go 不是运行 Bazel 构建的必要条件。不过,若需运行 go get 或 go mod tidy 等 Go 命令,仍需要本地 Go。样例保存在 Git 仓库中,先取得 Bazel examples 仓库,然后进入 examples/go-tutorial。
原文提供的项目目录为:
go-tutorial/
stage1/
stage2/
stage3/
第一阶段:构建并运行二进制目标
进入 stage1。BUILD 文件声明一个 go_binary,它从 hello.go 生成名为 hello 的可执行目标:
load("@rules_go//go:def.bzl", "go_binary")
go_binary(
name = "hello",
srcs = ["hello.go"],
)
对应 Go 源码:
package main
import "fmt"
func main() {
fmt.Println("Hello, Bazel! 💚")
}
构建目标并运行生成的文件:
bazel build //:hello
bazel-bin/hello_/hello
也可以用 bazel run //:hello 一步构建并启动。原文输出 “Hello, Bazel!”。该输出是源文示例记录,并非本稿执行所得。
理解项目结构与 BUILD 文件
常见项目会在每个 Bazel 包目录放一个 BUILD 或 BUILD.bazel 文件。它以 Starlark 编写,是受限的 Python 风格配置语言。BUILD 文件把希望构建的实体声明为目标;每个目标调用一个规则函数并提供属性。例如:
name是命令行使用的目标名。srcs列出源文件,路径相对于 BUILD 文件所在目录,使用斜杠。
规则定义 Bazel 怎样把输入转化成产物。go_binary 规则会生成 Go 编译与链接动作,输出二进制。Bazel 自带少数语言规则,其他语言常由独立规则集提供。
MODULE.bazel 与锁文件
每个阶段目录的 MODULE.bazel 声明依赖并标记模块根目录。它在模块解析方面与 Go 的 go.mod 有相似用途,但并不能简单视为相同文件。Bazel 项目不强制要有 go.mod;若团队仍使用 go get 或 go mod tidy,保留它可能有用。Go 模块导入及工具链细节超出本教程范围。
示例依赖 rules_go,因为 Bazel 不内建 Go 规则:
bazel_dep(
name = "rules_go",
version = "0.50.1",
)
Bazel 生成的 MODULE.bazel.lock 存储依赖版本、校验哈希与其他解析元数据,类似 Go 的 go.sum。原文建议将锁文件提交版本控制,保证团队解析到一致依赖;通常不应手动编辑。锁文件是版本稳定性保障的一部分,不等于其所用上游代码自动安全。
第二阶段:拆出 Go 库
stage2 把程序扩展成一个二进制目标加一个单独目录中的 fortune 库。Go 项目常按 package 分目录;Bazel 不强制这一布局,但遵循惯例可提高与其他 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))]
}
库目录自己的 BUILD 文件用 go_library 声明包名、源文件和导入路径;并设置 visibility,决定哪些其他 Bazel 包可依赖它:
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"],
)
//visibility:public 是面向整个工作区公开;它使任何包都能依赖此目标。若库仅供项目局部使用,发布项目时应评估是否需要这么宽的可见性。
主程序以同一导入路径引用该库:
package main
import (
"fmt"
"github.com/bazelbuild/examples/go-tutorial/stage2/fortune"
)
func main() {
fmt.Println(fortune.Get())
}
BUILD 中也必须声明该依赖:
load("@rules_go//go:def.bzl", "go_binary")
go_binary(
name = "print_fortune",
srcs = ["print_fortune.go"],
deps = ["//fortune"],
)
运行 bazel run //:print_fortune 可构建并启动。Bazel 要求在 deps 等属性中显式列出目标依赖,即使源码里也有 import。这让 Bazel 能预先建出动作图,再缓存动作结果或把动作交给远端执行,而不必内建每种语言的源码分析器。
读懂标签和包边界
标签是 Bazel 用于指代目标或文件的字符串,例如 //fortune、//:print_fortune 与 @rules_go//go:def.bzl。通常它包含仓库、包和目标三部分:
- 仓库名位于
@与//之间;省略时指当前仓库。由于历史命名,Bazel 文档有时把 module 与 repository 互称。 - 包名位于
//与:之间,是从含MODULE.bazel的模块根到 BUILD 目录的斜线路径。 - 目标名位于冒号之后。若和包名最后一段相同,可以省略,例如
//fortune:fortune与//fortune等价;同包引用时包名也可省略。
一个 Bazel 包由一个顶层目录的 BUILD 文件定义。包可以包含没有自己 BUILD 文件的子目录;一旦子目录也有 BUILD 文件,它就成为独立包。Go 项目常按目录分别放 BUILD 文件与 Go package。命令行中 //... 可匹配仓库中所有包的目标,例如 bazel build //...。
第三阶段:添加 go_test
stage3 在同一个 fortune Go package 中新增测试。测试读取非导出的 fortunes,因此测试源文件需与实现共同编译到相同 package:
package fortune
import (
"slices"
"testing"
)
func TestGet(t *testing.T) {
msg := Get()
if i := slices.Index(fortunes, msg); i < 0 {
t.Errorf("Get returned %q, not one of the expected messages", msg)
}
}
BUILD 文件先定义库,再用 go_test 的 embed 属性把库目标带入测试目标:
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"],
)
这里的 embed 是 rules_go 的目标组合属性,与 Go 标准库的 embed 包并非同一概念。Go embed 文件列表由规则属性 embedsrcs 指定。运行 bazel test //fortune:fortune_test 可执行这个测试;bazel test //... 则遍历并测试整个工作区,也会构建没有测试的目标,因此可以发现部分编译问题。
示例测试只检验 Get 返回的消息属于允许列表。它不是统计测试,不验证随机分布;若测试只需确认该函数返回有效消息,这个断言足够。Go 的 slices 标准库包从 Go 1.21 加入(见 Go 1.21 发行说明);源文没有明确展示 Go 工具链声明,需让 rules_go 与项目实际工具链匹配。
收尾与延伸
通过三个阶段,项目从一个 Go 二进制,扩展到独立库和测试。核心概念是:MODULE.bazel 管理 Bazel 模块依赖;BUILD 文件用目标描述输入、依赖与可见性;标签连接仓库中的目标;规则集为 Bazel 提供 Go 编译、链接与测试动作。进一步阅读可转向 rules_go 核心规则、外部依赖管理,以及 Bazel 官方 C++、Java、Android 和 iOS 教程。
许可与署名文件:Apache License 2.0 完整文本。











暂无评论内容