Simple hello world program using Bazel and Go lang

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.

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 https://github.com/nixclix/basil which will evolve over time. At the time of writing this post, the latest commit is https://github.com/nixclix/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.

0
Subscribe to my newsletter

Read articles from Nikhil Dev Chunchu directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Nikhil Dev Chunchu
Nikhil Dev Chunchu