Project: /_project.yaml Book: /_book.yaml
{# disableFinding(LINE_OVER_80_LINK) #} {# disableFinding(“below”) #}
{% include “_buttons.html” %}
This tutorial introduces you to the basics of Bazel by showing you how to build a Go (Golang) project. You‘ll learn how to set up your workspace, build a small program, import a library, and run its test. Along the way, you’ll learn key Bazel concepts, such as targets and BUILD
files.
Estimated completion time: 30 minutes
Before you get started, first install bazel if you haven't done so already.
You can check if Bazel is installed by running bazel version
in any directory.
You don't need to install Go to build Go projects with Bazel. The Bazel Go rule set automatically downloads and uses a Go toolchain instead of using the toolchain installed on your machine. This ensures all developers on a project build with same version of Go.
However, you may still want to install a Go toolchain to run commands like go get
and go mod tidy
.
You can check if Go is installed by running go version
in any directory.
The Bazel examples are stored in a Git repository, so you‘ll need to install Git if you haven’t already. To download the examples repository, run this command:
git clone https://github.com/bazelbuild/examples
The sample project for this tutorial is in the examples/go-tutorial
directory. See what it contains:
go-tutorial/ └── stage1 └── stage2 └── stage3
There are three subdirectories (stage1
, stage2
, and stage3
), each for a different section of this tutorial. Each stage builds on the previous one.
Start in the stage1
directory, where we'll find a program. We can build it with bazel build
, then run it:
$ 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! 💚
We can also build run the program with a single bazel run
command:
$ 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! 💚
Take a look at the project we just built.
hello.go
contains the Go source code for the program.
package main
import "fmt"
func main() {
fmt.Println("Hello, Bazel! 💚")
}
BUILD
contains some instructions for Bazel, telling it what we want to build. You'll typically write a file like this in each directory. For this project, we have a single go_binary
target that builds our program from hello.go
.
load("@rules_go//go:def.bzl", "go_binary") go_binary( name = "hello", srcs = ["hello.go"], )
MODULE.bazel
tracks your project‘s dependencies. It also marks your project’s root directory, so you‘ll only write one MODULE.bazel
file per project. It serves a similar purpose to Go’s go.mod
file. You don‘t actually need a go.mod
file in a Bazel project, but it may still be useful to have one so that you can continue using go get
and go mod tidy
for dependency management. The Bazel Go rule set can import dependencies from go.mod
, but we’ll cover that in another tutorial.
Our MODULE.bazel
file contains a single dependency on rules_go, the Go rule set. We need this dependency because Bazel doesn't have built-in support for Go.
bazel_dep( name = "rules_go", version = "0.50.1", )
Finally, MODULE.bazel.lock
is a file generated by Bazel that contains hashes and other metadata about our dependencies. It includes implicit dependencies added by Bazel itself, so it‘s quite long, and we won’t show it here. Just like go.sum
, you should commit your MODULE.bazel.lock
file to source control to ensure everyone on your project gets the same version of each dependency. You shouldn't need to edit MODULE.bazel.lock
manually.
Most of your interaction with Bazel will be through BUILD
files (or equivalently, BUILD.bazel
files), so it's important to understand what they do.
BUILD
files are written in a scripting language called Starlark, a limited subset of Python.
A BUILD
file contains a list of targets. A target is something Bazel can build, like a binary, library, or test.
A target calls a rule function with a list of attributes to describe what should be built. Our example has two attributes: name
identifies the target on the command line, and srcs
is a list of source file paths (slash-separated, relative to the directory containing the BUILD
file).
A rule tells Bazel how to build a target. In our example, we used the go_binary
rule. Each rule defines actions (commands) that generate a set of output files. For example, go_binary
defines Go compile and link actions that produce an executable output file.
Bazel has built-in rules for a few languages like Java and C++. You can find their documentation in the Build Encyclopedia. You can find rule sets for many other languages and tools on the Bazel Central Registry (BCR).
Move onto the stage2
directory, where we'll build a new program that prints your fortune. This program uses a separate Go package as a library that selects a fortune from a predefined list of messages.
go-tutorial/stage2 ├── BUILD ├── MODULE.bazel ├── MODULE.bazel.lock ├── fortune │ ├── BUILD │ └── fortune.go └── print_fortune.go
fortune.go
is the source file for the library. The fortune
library is a separate Go package, so its source files are in a separate directory. Bazel doesn‘t require you to keep Go packages in separate directories, but it’s a strong convention in the Go ecosystem, and following it will help you stay compatible with other Go tools.
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))]
}
The fortune
directory has its own BUILD
file that tells Bazel how to build this package. We use go_library
here instead of go_binary
.
We also need to set the importpath
attribute to a string with which the library can be imported into other Go source files. This name should be the repository path (or module path) concatenated with the directory within the repository.
Finally, we need to set the visibility
attribute to ["//visibility:public"]
. visibility
may be set on any target. It determines which Bazel packages may depend on this target. In our case, we want any target to be able to depend on this library, so we use the special value //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"], )
You can build this library with:
$ bazel build //fortune
Next, see how print_fortune.go
uses this package.
package main
import (
"fmt"
"github.com/bazelbuild/examples/go-tutorial/stage2/fortune"
)
func main() {
fmt.Println(fortune.Get())
}
print_fortune.go
imports the package using the same string declared in the importpath
attribute of the fortune
library.
We also need to declare this dependency to Bazel. Here's the BUILD
file in the stage2
directory.
load("@rules_go//go:def.bzl", "go_binary") go_binary( name = "print_fortune", srcs = ["print_fortune.go"], deps = ["//fortune"], )
You can run this with the command below.
bazel run //:print_fortune
The print_fortune
target has a deps
attribute, a list of other targets that it depends on. It contains "//fortune"
, a label string referring to the target in the fortune
directory named fortune
.
Bazel requires that all targets declare their dependencies explicitly with attributes like deps
. This may seem cumbersome since dependencies are also specified in source files, but Bazel's explictness gives it an advantage. Bazel builds an action graph containing all commands, inputs, and outputs before running any commands, without reading any source files. Bazel can then cache action results or send actions for remote execution without built-in language-specific logic.
A label is a string Bazel uses to identify a target or a file. Labels are used in command line arguments and in BUILD
file attributes like deps
. We've seen a few already, like //fortune
, //:print-fortune
, and @rules_go//go:def.bzl
.
A label has three parts: a repository name, a package name, and a target (or file) name.
The repository name is written between @
and //
and is used to refer to a target from a different Bazel module (for historical reasons, module and repository are sometimes used synonymously). In the label, @rules_go//go:def.bzl
, the repository name is rules_go
. The repository name can be omitted when referring to targets in the same repository.
The package name is written between //
and :
and is used to refer to a target in from a different Bazel package. In the label @rules_go//go:def.bzl
, the package name is go
. A Bazel package is a set of files and targets defined by a BUILD
or BUILD.bazel
file in its top-level directory. Its package name is a slash-separated path from the module root directory (containing MODULE.bazel
) to the directory containing the BUILD
file. A package may include subdirectories, but only if they don't also contain BUILD
files defining their own packages.
Most Go projects have one BUILD
file per directory and one Go package per BUILD
file. The package name in a label may be omitted when referring to targets in the same directory.
The target name is written after :
and refers to a target within a package. The target name may be omitted if it's the same as the last component of the package name (so //a/b/c:c
is the same as //a/b/c
; //fortune:fortune
is the same as //fortune
).
On the command-line, you can use ...
as a wildcard to refer to all the targets within a package. This is useful for building or testing all the targets in a repository.
# Build everything $ bazel build //...
Next, move to the stage3
directory, where we'll add a test.
go-tutorial/stage3 ├── BUILD ├── MODULE.bazel ├── MODULE.bazel.lock ├── fortune │ ├── BUILD │ ├── fortune.go │ └── fortune_test.go └── print-fortune.go
fortune/fortune_test.go
is our new test source file.
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) } }
This file uses the unexported fortunes
variable, so it needs to be compiled into the same Go package as fortune.go
. Look at the BUILD
file to see how that works:
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"], )
We have a new fortune_test
target that uses the go_test
rule to compile and link a test executable. go_test
needs to compile fortune.go
and fortune_test.go
together with the same command, so we use the embed
attribute here to incorporate the attributes of the fortune
target into fortune_test
. embed
is most commonly used with go_test
and go_binary
, but it also works with go_library
, which is sometimes useful for generated code.
You may be wondering if the embed
attribute is related to Go‘s embed
package, which is used to access data files copied into an executable. This is an unfortunate name collision: rules_go’s embed
attribute was introduced before Go's embed
package. Instead, rules_go uses the embedsrcs
to list files that can be loaded with the embed
package.
Try running our test with 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.
You can use the ...
wildcard to run all tests. Bazel will also build targets that aren‘t tests, so this can catch compile errors even in packages that don’t have tests.
$ bazel test //...
In this tutorial, we built and tested a small Go project with Bazel, and we learned some core Bazel concepts along the way.