用 Bazel 组织 Go 目标、依赖与测试

用 Bazel 组织 Go 目标、依赖与测试

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

Bazel教程从stage1的go_binary开始,在stage2添加go_library和deps,在stage3添加嵌入库目标的go_test,并通过标签连接。
原创示意图:三阶段目标关系;不是构建结果截图。

预计阅读时间约 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 教程。

来源:Bazel Documentation contributors,《Bazel Tutorial: Build a Go Project》,Bazel 官方文档。原文叙述内容依 CC BY 4.0 许可;原文代码样例依 Apache License 2.0 许可。本文为中文译编,整合并补充了可见性和工具链边界,保留原作者、规则版本、示例及测试输出的来源归属。教程中的本地克隆、构建与测试输出是源页记录,不是本文实测结果;本文未运行 Bazel、Go 或示例测试。

许可与署名文件:Apache License 2.0 完整文本。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容