Bazel 教程:构建 Go 项目

报告问题 查看源代码 每夜版 · 8.4 · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

本教程将通过演示如何构建 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 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++ 等少数几种语言提供了内置规则。您可以在《Build Encyclopedia》中找到相关文档。您可以在 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 软件包是一组文件和目标,由其顶层目录中的 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.Inde<x(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 软件包之前引入的。相反,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 with bzlmod