Let's Go Further Building, versioning and quality control › Building binaries
Previous · Contents · Next
Chapter 19.5.

Building binaries

So far we’ve been running our API using the go run command (or more recently, make run/api). But in this chapter we’re going to focus on explaining how to build an executable binary that you can distribute and run on other machines without needing the Go toolchain installed.

To build a binary we need to use the go build command. As a simple example, usage looks like this:

$ go build -o=./bin/api ./cmd/api

When we run this command, go build will compile the cmd/api package (and any dependent packages) into files containing machine code, and then link these together to form an executable binary. In the command above, the executable binary will be output to ./bin/api.

For convenience, let’s add a new build/api rule to our makefile which runs this command, like so:

File: Makefile
...

# ==================================================================================== #
# BUILD
# ==================================================================================== #

## build/api: build the cmd/api application
.PHONY: build/api
build/api:
	@echo 'Building cmd/api...'
	go build -o=./bin/api ./cmd/api

Once that’s done, go ahead and execute the make build/api rule. You should see that an executable binary file gets created at ./bin/api.

$ make build/api 
Building cmd/api...
go build -o=./bin/api ./cmd/api
$ ls -l ./bin/
total 10228
-rwxrwxr-x 1 alex alex 10470419 Apr 18 16:05 api

And you should be able to run this executable to start your API application, passing in any command-line flag values as necessary. For example:

$ ./bin/api -port=4040 -db-dsn=postgres://greenlight:pa55word@localhost/greenlight
time=2023-09-10T10:59:13.722+02:00 level=INFO msg="database connection pool established"
time=2023-09-10T10:59:13.722+02:00 level=INFO msg="starting server" addr=:4040 env=development

Reducing binary size

If you take a closer look at the executable binary you’ll see that it weighs in at 10470419 bytes (about 10.5MB).

$ ls -l ./bin/api 
-rwxrwxr-x 1 alex alex 10470419 Apr 18 16:05 ./bin/api

It’s possible to reduce the binary size by around 25% by instructing the Go linker to strip symbol tables and DWARF debugging information from the binary. We can do this as part of the go build command by using the linker flag -ldflags="-s" as follows:

File: Makefile
...

# ==================================================================================== #
# BUILD
# ==================================================================================== #

## build/api: build the cmd/api application
.PHONY: build/api
build/api:
	@echo 'Building cmd/api...'
	go build -ldflags='-s' -o=./bin/api ./cmd/api

If you run make build/api again, you should now find that the binary is noticeably smaller (in my case about 7.6MB).

$ make build/api 
Building cmd/api...
go build -ldflags='-s' -o=./bin/api ./cmd/api
$ ls -l ./bin/api 
-rwxrwxr-x 1 alex alex 7618560 Apr 18 16:08 ./bin/api

It’s important to be aware that stripping out this information will make it harder to debug an executable using a tool like Delve or gdb. But, generally, it’s not often that you’ll need to do this — and there’s even an open proposal from Rob Pike to make omitting DWARF information the default behavior of the linker in the future.

Cross-compilation

By default, the go build command will output a binary suitable for use on your local machine’s operating system and architecture. But it also supports cross-compilation, so you can generate a binary suitable for use on a different machine. This is particularly useful if you’re developing on one operating system and deploying on another.

To see a list of all the operating system/architecture combinations that Go supports, you can run the go tool dist list command like so:

$ go tool dist list
aix/ppc64
android/386
android/amd64
android/arm
android/arm64
darwin/amd64
...

And you can specify the operating system and architecture that you want to create the binary for by setting GOOS and GOARCH environment variables when running go build. For example:

$ GOOS=linux GOARCH=amd64 go build {args}

In the next section of the book, we’re going to walk through how to deploy an executable binary on an Ubuntu Linux server hosted by DigitalOcean. For this, we’ll need a binary which is designed to run on a machine with a linux/amd64 OS and architecture combination.

So let’s update our make build/api rule so that it creates two binaries — one for use on your local machine, and another for deploying to the Ubuntu Linux server.

File: Makefile
...

# ==================================================================================== #
# BUILD
# ==================================================================================== #

## build/api: build the cmd/api application
.PHONY: build/api
build/api:
	@echo 'Building cmd/api...'
	go build -ldflags='-s' -o=./bin/api ./cmd/api
	GOOS=linux GOARCH=amd64 go build -ldflags='-s' -o=./bin/linux_amd64/api ./cmd/api

If you’re following along, go ahead and run make build/api again.

You should see that two binaries are now created — with the cross-compiled binary located under the ./bin/linux_amd64 directory, like so:

$ make build/api 
Building cmd/api...
go build -ldflags='-s' -o=./bin/api ./cmd/api
GOOS=linux GOARCH=amd64 go build -ldflags='-s' -o=./bin/linux_amd64/api ./cmd/api
$ tree ./bin
./bin
├── api
└── linux_amd64
    └── api

As a general rule, you probably don’t want to commit your Go binaries into version control alongside your source code as they will significantly inflate the size of your repository.

So, if you’re following along, let’s quickly add an additional rule to the .gitignore file which instructs Git to ignore the contents of the bin directory.

$ echo 'bin/' >> .gitignore
$ cat .gitignore
.envrc
bin/

Additional information

Build caching

It’s important to note that the go build command caches build output in the Go build cache. This cached output will be reused again in future builds where appropriate, which can significantly speed up the overall build time for your application.

If you’re not sure where your build cache is, you can check by running the go env GOCACHE command:

$ go env GOCACHE
/home/alex/.cache/go-build

You should also be aware that the build cache does not automatically detect any changes to C libraries that your code imports with cgo. So, if you’ve changed a C library since the last build, you’ll need to use the -a flag to force all packages to be rebuilt when running go build. Alternatively, you could use go clean to purge the cache:

$ go build -a -o=/bin/foo ./cmd/foo        # Force all packages to be rebuilt
$ go clean -cache                          # Remove everything from the build cache