Bazel 教程:构建 Go 项目

报告问题 查看源代码 每夜 build · 8.0 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

本教程将通过介绍如何构建 Go (Golang) 项目,向您介绍 Bazel 的基础知识。您将学习如何设置工作区、构建小程序、导入库以及运行其测试。在此过程中,您将学习一些重要的 Bazel 概念,例如目标和 BUILD 文件。

预计完成时间:30 分钟

准备工作

安装 Bazel

在开始之前,如果您尚未安装 bazel,请先执行此操作。

您可以在任何目录中运行 bazel version 来检查 Bazel 是否已安装。

安装 Go(可选)

您无需安装 Go 即可使用 Bazel 构建 Go 项目。Bazel Go 规则集会自动下载并使用 Go 工具链,而不是使用您机器上安装的工具链。这样可以确保项目中的所有开发者使用相同版本的 Go 进行构建。

不过,您可能仍需要安装 Go 工具链,以运行 go getgo mod tidy 等命令。

您可以在任何目录中运行 go version 来检查是否已安装 Go。

获取示例项目

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.mod 文件可能仍然很有用,这样您就可以继续使用 go getgo 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",这是一个标签字符串,用于引用名为 fortunefortune 目录中的目标。

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 软件包相关,该软件包用于访问复制到可执行文件中的数据文件。这是一个不幸的名称冲突: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 模块和工具链,请参阅 Go with bzlmod