An Intro to Building a Go Application with Bazel

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!