-
Notifications
You must be signed in to change notification settings - Fork 4.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
site: add a beginner tutorial for Go #23958
Changes from 1 commit
684630d
e8696da
e2dbc3d
1189b90
31cc52e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,343 @@ | ||
Project: /_project.yaml | ||
Book: /_book.yaml | ||
|
||
# Bazel Tutorial: Build a Go Project | ||
|
||
{% 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 | ||
|
||
## What you'll learn | ||
|
||
## Before you begin | ||
|
||
### Install Bazel | ||
|
||
Before you get started, first [install bazel](/install) if you haven't done so already. | ||
|
||
You can check if Bazel is installed by running `bazel version` in any directory. | ||
|
||
### Install Go (optional) | ||
|
||
You don't need to [install Go](https://go.dev/doc/install) 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. | ||
|
||
### Get the sample project | ||
|
||
The Bazel examples are stored in a Git repository, so you'll need to [install Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) if you haven't already. To download the examples repository, run this command: | ||
|
||
```posix-terminal | ||
git clone https://github.com/bazelbuild/examples | ||
``` | ||
|
||
The sample project for this tutorial is in the `examples/go-tutorial` directory. Let's see what it contains: | ||
|
||
```none | ||
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. | ||
|
||
## Build with Bazel | ||
|
||
Let's start in the `stage1` directory, where we'll find a simple program. We can build it with `bazel build`, then run it: | ||
|
||
```posix-shell | ||
$ 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: | ||
|
||
```posix-shell | ||
$ 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! 💚 | ||
``` | ||
|
||
### Understanding project structure | ||
|
||
Let's take a look at the project we just built. | ||
|
||
`hello.go` contains the Go source code for the program. | ||
|
||
```go | ||
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`. | ||
|
||
```bazel | ||
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](https://github.com/bazelbuild/rules_go), the Go rule set. We need this dependency because Bazel doesn't have built-in support for Go. | ||
|
||
```bazel | ||
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 should not need to edit `MODULE.bazel.lock` manually. | ||
|
||
### Understand the BUILD file | ||
|
||
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](https://bazel.build/rules/language), a limited subset of Python. | ||
|
||
A `BUILD` file contains a list of [targets](https://bazel.build/reference/glossary#target). A target is something Bazel can build, like a binary, library, or test. | ||
|
||
A target calls a rule function with a list of [attributes](https://bazel.build/reference/glossary#attribute) 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](https://bazel.build/reference/glossary#rule) tells Bazel how to build a target. In our example, we used the [`go_binary`](https://github.com/bazelbuild/rules_go/blob/master/docs/go/core/rules.md#go_binary) rule. Each rule defines [actions](https://bazel.build/reference/glossary#action) (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](https://bazel.build/reference/be/overview#rules). You can find rule sets for many other languages and tools on the [Bazel Central Registry (BCR)](https://registry.bazel.build/). | ||
|
||
## Add a library | ||
|
||
Let's 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. | ||
|
||
```none | ||
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. | ||
|
||
```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))] | ||
} | ||
``` | ||
|
||
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`](https://bazel.build/concepts/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`. | ||
|
||
```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"], | ||
) | ||
``` | ||
|
||
You can build this library with: | ||
|
||
```posix-shell | ||
$ bazel build //fortune | ||
``` | ||
|
||
Next, let's see how `print_fortune.go` uses this package. | ||
|
||
```go | ||
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. | ||
|
||
```bazel | ||
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. | ||
|
||
```posix-shell | ||
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](https://bazel.build/reference/glossary#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](https://bazel.build/remote/rbe) without builtin language-specific logic. | ||
|
||
### Understanding labels | ||
|
||
A [label](https://bazel.build/reference/glossary#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](https://bazel.build/reference/glossary#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. | ||
|
||
```posix-shell | ||
# Build everything | ||
$ bazel build //... | ||
``` | ||
|
||
## Test your project | ||
|
||
Next, let's move to the `stage3` directory, where we'll add a test. | ||
|
||
```none | ||
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. | ||
|
||
```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) | ||
} | ||
} | ||
``` | ||
|
||
This file uses the unexported `fortunes` variable, so it needs to be compiled into the same Go package as `fortune.go`. Let's look at the `BUILD` file to see how that works: | ||
|
||
```bazel | ||
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`](https://pkg.go.dev/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. | ||
|
||
Let's try running our test with `bazel test`: | ||
|
||
```posix-shell | ||
$ 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. | ||
|
||
```posix-shell | ||
$ bazel test //... | ||
``` | ||
|
||
## Conclusion and further reading | ||
|
||
In this tutorial, we built and tested a small Go project with Bazel, and we learned some core Bazel concepts along the way. | ||
|
||
- To get started building other applications with Bazel, see the tutorials for [C++](/start/cpp), [Java](/start/java), [Android](/start/android-app), and [iOS](/start/ios-app). | ||
- You can also check the list of [recommended rules](/rules) for other languages. | ||
- For more information on Go, see the [rules_go](https://github.com/bazelbuild/rules_go) module, especially the [Core Go rules](https://github.com/bazelbuild/rules_go/blob/master/docs/go/core/rules.md) docuemntation. | ||
jayconrod marked this conversation as resolved.
Show resolved
Hide resolved
|
||
- To learn more about working with modules outside your project, see [external dependencies](/docs/external). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could make sense to say Could you also mention external deps and reference the rules_go There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, made both changes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there actually missing content for
## What you'll learn
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I meant to take that heading out, now done. I initially followed some of the structure from the Java tutorial, but then it seemed better to just write the things you'll learn in the first paragraph.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Np!