An Intro to Building a Go Application with Bazel
So, I've been using Bazel for a little bit and it's a pretty neat build system. There are some things about it that are not super obvious and have struggled a bit with my learning curve, but wanted to share what I know so far to help you all out.
Setup
Let's start by initializing our Go module:
go mod init github.com/bgreeley/bazel-demo
This will generate your go.mod
file that will look like this:
module github.com/bgreeley/bazel-demo
go 1.16
So, now let's take a look at what we need for getting Bazel setup. There are two files we need to create at the root of your project: WORKSPACE
and BUILD.bazel
The WORKSPACE
file tells Bazel how to get sources from other projects. The BUILD.bazel
file is used to define various targets that can be built, run, or executed as part of your workflow. In our scenario, we're using the root BUILD.bazel
file to define a target to run Gazelle (which we'll get into a bit more in the next sections).
For our WORKSPACE
file, I pulled a version from Bazel's GitHub repo that sets up some pretty basic external dependencies. I haven't dove too deeply into each piece of this yet, so I'll have to gloss over this for now (although I'm sure I'll have another post soon that discusses this). You can find this here: https://github.com/bazelbuild/rules_go
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_archive(
name = "io_bazel_rules_go",
sha256 = "69de5c704a05ff37862f7e0f5534d4f479418afc21806c887db544a316f3cb6b",
urls = [
"https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.27.0/rules_go-v0.27.0.tar.gz",
"https://github.com/bazelbuild/rules_go/releases/download/v0.27.0/rules_go-v0.27.0.tar.gz",
],
)
load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
go_rules_dependencies()
go_register_toolchains(version = "1.16")
http_archive(
name = "bazel_gazelle",
sha256 = "222e49f034ca7a1d1231422cdb67066b885819885c356673cb1f72f748a3c9d4",
urls = [
"https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.22.3/bazel-gazelle-v0.22.3.tar.gz",
"https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.22.3/bazel-gazelle-v0.22.3.tar.gz",
],
)
load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
gazelle_dependencies()
For our BUILD.bazel
file, the only target I want to add here is one that will allow us to run Gazelle. Gazelle is a very cool tool that will analyze your directories and generate your BUILD.bazel
files in each package automatically. Trust me, this will save you a lot of time.
The file contents will look something like this:
load("@bazel_gazelle//:def.bzl", "gazelle")
# gazelle:go_naming_convention go_default_library
gazelle(
name = "gazelle",
prefix = "github.com/bgreeley/bazel-demo",
)
Building Our Application
Ok, now that we've done the setup, we can build our application - a simple book lookup service.
Our application will have two Bazel packages: one for the book service and the other for the book application itself. The book service will be a dependency of the book application.
Let's focus first on building the book service. We'll create our service.go
file in our internal/service
package:
package service
type service struct {
}
func NewBookService() *service {
return &service{}
}
type Book struct {
Title string
}
func (s *service) GetBook(title string) *Book {
return &Book{
Title: title,
}
}
If we run the following:
bazel run //:gazelle
This will create a BUILD.bazel
file in our service package that looks like this:
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["service.go"],
importpath = "github.com/bgreeley/bazel-demo/internal/service",
visibility = ["//visibility:public"],
)
We can now use this target as a dependency of our main application which we'll create a main.go
file in our cmd
package:
package main
import (
"fmt"
"github.com/bgreeley/bazel-demo/internal/service"
)
func main() {
service := service.NewBookService()
book := service.GetBook("The Water Dancer")
fmt.Println(book.Title)
}
We'll run Gazelle one more time and the BUILD.bazel
file will generate the following file:
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "go_default_library",
srcs = ["main.go"],
importpath = "github.com/bgreeley/bazel-demo/cmd",
visibility = ["//visibility:public"],
deps = ["//internal/service:go_default_library"],
)
go_binary(
name = "cmd",
embed = [":go_default_library"],
visibility = ["//visibility:public"],
)
There are a couple of cool things that Gazelle has done here:
- It will automatically add all of your libraries dependencies for you
- It will automatically add a Go binary that we can run in addition to the Go library when there is a main package configured.
To give a better sense of what we've done, here is what our directory structure looks like now:
├── BUILD.bazel
├── WORKSPACE
├── cmd
│ ├── BUILD.bazel
│ └── main.go
├── go.mod
└── internal
└── service
├── BUILD.bazel
└── service.go
Running Application
Now that we have our packages setup, we can run our application like this:
bazel run //cmd:cmd
And we see the output like this:
The Water Dancer
And that's it.
Tests
Just a quick follow up - you can also use Bazel to help run tests. We can add a service_test.go
file to our internal/service
package like this:
package service
import (
"testing"
)
func TestGetBook(t *testing.T) {
// Given
title := "Kafka on the Shore"
// When
book := NewBookService().GetBook(title)
// Then
if book.Title != title {
t.Fail()
}
}
And then we run Gazelle again like so:
bazel run //:gazelle
We'll see our BUILD.bazel
file was updated with a new go_test
target:
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = ["service.go"],
importpath = "github.com/bgreeley/bazel-demo/internal/service",
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = ["service_test.go"],
embed = [":go_default_library"],
)
We can now run this test like so:
bazel test //internal/service:go_default_test
And we'll see the output:
INFO: Analyzed 4 targets (0 packages loaded, 0 targets configured).
INFO: Found 3 targets and 1 test target...
INFO: Elapsed time: 0.115s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
//internal/service:go_default_test (cached) PASSED in 0.4s
Executed 0 out of 1 test: 1 test passes.
INFO: Build completed successfully, 1 total action
I hope this helps!