After writing the post on "Building with Go in a monorepo using Bazel, Gazelle and bzlmod" and sharing with some colleagues, I saw some growing interest in monorepo development. I learnt that not many people were still experienced enough to understand the benefits it could bring. So I decided to convert this to a series starting with this post on Simple hello world program using Bazel and Go lang
In this post, I will try to cover some extremely basic concepts of Bazel along with code snippets to get someone up and going in no time.
What is Bazel?
According to the official documentation
Bazel is an open-source build and test tool similar to Make, Maven, and Gradle. It uses a human-readable, high-level build language. Bazel supports projects in multiple languages and builds outputs for multiple platforms. Bazel supports large codebases across multiple repositories, and large numbers of users.
It has been in use at Google for decades and is thoroughly battle tested. You can read more about how I became aware of this on the post linked above.
Step 1: Setting up repository
For the purpose of this series I have created a repository at github.com/nikhildev/basil which will evolve over time. At the time of writing this post, the latest commit is https://github.com/nikhildev/basil/tree/d8af2aea6fb8b692f735f9959429df9fcd28422b
So, go ahead an create a new git repo on any provider you want to choose
For the .gitignore
I highly recommend adding the following so as to not include unnecessary files in your commits
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
bazel-*
/.ijwb
/.clwb
/.idea
/.project
*.swp
/.bazelrc.user
# macOS-specific excludes
.DS_Store
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
# env file
.env
Step 2: Adding some Bazel boilerplate
Starting Bazel 6.3 you do not need to have a WORKSPACE file anymore. So, we will start with creating the following at the root of the repo
MODULE.bazel
"""Building go applications in a monorepo environment"""
module(name = "basil", version = "0.0.0")
http_file = use_repo_rule(
"@bazel_tools//tools/build_defs/repo:http.bzl", "http_file"
)
http_archive = use_repo_rule(
"@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive"
)
# https://github.com/bazelbuild/rules_go/blob/master/docs/go/core/bzlmod.md
bazel_dep(name = "rules_go", version = "0.50.1")
bazel_dep(name = "gazelle", version = "0.39.1")
GO_VERSION = "1.23.2"
go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk")
go_sdk.download(version = GO_VERSION)
go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps")
go_deps.from_file(go_mod = "//:go.mod")
Here is where we set the go version we want to use as well as the gazelle and rules_go version.
We will be using Gazelle for BUILD file management. Gazelle makes generating build file quite convenient. You can read more about it here
BUILD.bazel
load("@gazelle//:def.bzl", "gazelle")
gazelle(name = "gazelle")
gazelle(
name = "gazelle-update-repos",
args = [
"-from_file=go.mod",
"-to_macro=deps.bzl%go_dependencies",
"-prune",
],
command = "update-repos",
)
This should be at the root of the repo. This instructs gazelle on the source of the go mod file and other processes to run
Next we create 3 more files with the respective contents. Don't worry about what these do for now.
.bazelignore
runfiles/
bzlmod/
.bazelrc
# Enable Bzlmod for every Bazel command
common --enable_bzlmod
.bazelversion
7.3.1
Alright, by this point you should have everything that's needed to get some basics working. Now if you run bazel build //...
at the root, bazel should be able traverse through the repo and build any packages that it discovers. Bazel caches package outputs from earlier builds, so any subsequent build from here on should be extremely fast.
Next up, let's get cracking on that actual hello world program.
Step 3: Writing a helloworld package
For the basic organisation of code we will be writing all the Go code in a directory called /packages
. This way any references in other parts of the code can refer to it as //packages/...
Let's create a directory in the packages directory called helloworld
and add the following
helloworld.go
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello World!")
}
BUILD.bazel
load("@rules_go//go:def.bzl", "go_binary", "go_library")
go_binary(
name = "helloworld",
embed = [":helloworld_lib"],
importpath = "basil/packages/helloworld",
visibility = ["//visibility:private"],
)
go_library(
name = "helloworld_lib",
srcs = ["helloworld.go"],
importpath = "basil/packages/helloworld_lib",
visibility = ["//visibility:private"],
)
The go program is fairly simple. We have a simple main function that simple prints a hello world message.
The bit we are interested in is the BUILD.bazel
file. Let's go over this block by block and try to understand what it means.
The first line loads the go_binary and go_library macros from rules_go
. You will find these being used later in the code.
In line 10 we are defining a library called helloworld_lib
and specify the sources of the library as helloworld.go
. If this package needs to be me made importable, then you can also specify the path it will be available at which is basil/packages/helloworld_lib
. Then comes the visibility and here we are specifying that the library is private only visible within this package. In the future posts we might look into how to change these parameters to use dependencies from other packages.
In line 3 we then use the go_binary macro from rules_go to generate a binary for our program. Here we specify the library that we have defined earlier in the embed parameter. The package visibility applies to binaries as well.
Step 4: Build and Run
And that's it. Voila! You can run the binary by first building it using bazel build //packages/helloworld
followed by bazel run //packages/helloworld
If you like this post and would like to get future posts that build upon this as a part of the series, please subscribe and share this post.
Top comments (0)