Building And Compiling Go Applications

Overview

In this chapter, we dive into the mechanics of building and compiling Go applications. Go’s ability to produce efficient, statically linked binaries across multiple platforms makes it an excellent choice for various deployment environments. You will learn how to use the go build and go install commands to compile your Go code into standalone executables. We will also explore how to embed build metadata like version numbers and commit SHA-1 hashes into binaries using linker flags. Additionally, the chapter covers build tags, a powerful feature that enables platform-specific compilation and fine-grained control over how your code is built. By the end of this chapter, you will be well-equipped to compile Go applications for both development and production, with support for cross-platform distribution.

Building And Compiling

Go is a compiled language. This means that before we can run a Go application it must be compiled to an executable capable of running on the current platform.

Running Go Code

When in development it is common to use go run some-file.go to run Go applications.

But you said we have to compile applications before we can run them?

When we use go run to run a Go application it silently creates a binary, executes the binary, and then deletes the binary.

This makes the process of running code locally easy for developers.

Why Not Just Use `go Run`?

While using go run is great for local development, it is not ideal for applications that are to be given to other people.

go run has the following requirements:

  • Needs Go installed, and potentially the same version
  • Requires all dependencies be present
  • Doesn’t allow for custom hooks, as we’ll see later

Building A Binary

Go ships with some pretty powerful build tools that can be accessed with the go build command.

When building a binary with the go build command, Go will build a fully bundled, statically linked, executable.

Go Build

$GOPATH/src/examples/simple/main.go

package main

import "fmt"

func main() {
	fmt.Println("Hello, World!")
}
go build
-rw-r--r--@ 1 markbates  staff    74B May 12 15:04 main.go
-rwxr-xr-x@ 1 markbates  staff   1.6M May 12 15:06 simple*

Specify A Binary Name

By default go build will name the executable after the current folder and place it in the current folder.

We can change this behavior with the -o flag.

go build -o bin/hello
drwxr-xr-x@ 3 markbates  staff   102B May 12 15:09 bin/
-rw-r--r--@ 1 markbates  staff    74B May 12 15:04 main.go

./bin:
-rwxr-xr-x@ 1 markbates  staff   1.6M May 12 15:09 hello*

Installing A Binary

When we setup our GOPATH we are told to add $GOPATH/bin to our $PATH. The reason for this is this is where the go install command places binaries.

We can use go install in exactly the same way as go build, the difference being that instead of placing a binary in the current directory it will build it and place it in the $GOPATH/bin directory.

Build Tags

Using tags at build time allows us to control how are binary is compiled, and what it does. For example, complile one binary for development and another for production; each with different levels of logging.

We can also control behavior based on platform, but we’ll look at that later.

Custom Build Tags

Let’s create a simple program that prints out a greeting.


// main.go
package main

import "fmt"

func main() {
	fmt.Println(greeting)
}

Same Method

Now let’s create two other files, happy.go and sad.go, that will contain the various greetings we wish to display.


package main

import "fmt"

func init() {
	fmt.Println("happy.go")
}

var greeting = "It's so great to see you!!"

package main

import "fmt"

func init() {
	fmt.Println("sad.go")
}

var greeting = "I'm so sorry to see you. :("

Error On Build

Attempting to run go build on our application will result in an error.

./sad.go:10: greeting redeclared in this block
 previous declaration at ./happy.go:10

Using Build Tags

Using build tags we can turn on/off each file when building the binary.

Build tags are placed at the top of a .go file, above the package declaration.

//go:build tag1 || tag2 || tag3

package foo

Note: Prior to Go 1.17, the legacy syntax // +build was used. The gofmt tool automatically converts the old syntax to the new //go:build format. You may encounter the old syntax in older codebases.

Specifying Build Tags

We can update both the happy.go and the sad.go files to using the happy tag for happy.go or !happy for sad.go.


//go:build happy

// happy.go
package main

The happy build tag is equivalent of saying “only build this file when there is a happy tag”.


//go:build !happy

// sad.go
package main

The !happy build tag is the equivalent of saying “don’t build this file when there is a happy tag”.

Using Build Tags

If we were to run the default go build command the application would compile. When run it would print out the following:

sad.go
I'm so sorry to see you. :(

Since we did not specify the happy flag the happy.go file was not compiled, but the sad.go one was.

The Results

To compile the application with the happy tag we need to change our go build command.

go build -tags happy

Now when run we get the happy greeting.

happy.go
It's so great to see you!!

Pragmas

Build tags are just pragmas in Go. There are several useful pragmas to know, and build tags are just one of them.

Dave Cheney has a great article on pragmas. As well as you can look in the standard library lexer to see what pragmas they parse for.

Building Cross Platform Binaries

The Go build tools, by default, build binaries for the current platform you are on. However, it is possible to compile binaries for multiple different platforms using just the standard Go tools and a couple of environment variables and/or build tags.

Building With Environment Variables

The Go tools use two different environment variables to decide which binary to build.

$GOOS

The $GOOS variable controls which operating system to build for. Examples are darwin, linux, windows, etc..

$GOARCH

The $GOARCH variable controls which architecture to build for. Examples are 386, arm, amd64, etc…

Available Platforms


aix/ppc64        freebsd/arm64   linux/ppc64le  openbsd/arm64
android/386      freebsd/riscv64 linux/riscv64  openbsd/mips64
android/amd64    illumos/amd64   linux/s390x    openbsd/ppc64
android/arm      ios/amd64       netbsd/386     openbsd/riscv64
android/arm64    ios/arm64       netbsd/amd64   plan9/386
darwin/amd64     js/wasm         netbsd/arm     plan9/amd64
darwin/arm64     linux/386       netbsd/arm64   plan9/arm
dragonfly/amd64  linux/amd64     openbsd/386    solaris/amd64
freebsd/386      linux/arm       openbsd/amd64  wasip1/wasm
freebsd/amd64    linux/arm64     openbsd/arm    windows/386
freebsd/arm      linux/loong64   openbsd/arm64  windows/amd64
                 linux/mips                     windows/arm
                 linux/mips64                   windows/arm64
                 linux/mips64le
                 linux/mipsle
                 linux/ppc64

Run go tool dist list to see all available platforms for your Go version

Specifying GOOS

GOOS=windows go build
-rw-r--r--@ 1 markbates  staff    74B May 12 15:04 main.go
-rwxr-xr-x@ 1 markbates  staff   1.6M May 12 16:18 simple.exe*

Cross Platform Build Tags

Using environment variables lets us decide which platform and architecture we should build a binary for. But what about compiling different code for different platforms?

We can accomplish this using the build tags that we already learned about.

Specifying Platform Build Tags

Here we mark the happy.go file with darwin to indicate to the compiler that it should compile this file when compiling for OS X systems.


//go:build darwin

// happy.go
package main

The sad.go file should be compiled when building for linux.


//go:build linux

// sad.go
package main

Detecting Cross Platform Build Tags

The cross platform build tags are automatically linked to $GOOS and $GOARCH.

This means that, unlike our custom build tags, we don’t need to pass them in when compiling the binary.

Cross Platform File Names

The Go tools also allow us to control which files we want to build for which platforms using file names instead of build tags.

-rw-r--r--@ 1 markbates  staff   136B May 12 17:36 greeting_darwin.go
-rw-r--r--@ 1 markbates  staff   134B May 12 17:35 greeting_linux.go
-rw-r--r--@ 1 markbates  staff    78B May 12 17:35 main.go

Reference Articles

The following articles we wrote for Digital Ocean go much deeper on each topic:

Modifying Built Source With `ldflags`

It is quite common for applications to have to information, such as a version number, or an “environment” such as development or testing.

When building a binary for distribution we can use the -ldflags flag to burn that information into the built binary.

Embedding The External Values


package main

import "fmt"

var version = "development"

func main() {
	fmt.Printf("Version: %s\n", version)
}
$ go build -ldflags "-X main.version=1.0.0" -o bin/hello
$ ./hello
Version: 1.0.0

You can only embed string data using the ldflags method.

Beware Constants

It might seem like using a const instead of a var is more logical, but this will fail (silently):


package main

import "fmt"

const version = "development"

func main() {
	fmt.Printf("Version: %s\n", version)
}
$ go build -ldflags "-X main.version=1.0.0" -o bin/hello
$ ./bin/hello
Version: development

Real Life Example

Many Go web frameworks use -tags and -ldflags when building binaries:

$ go build -v -tags development -o bin/foo \
-ldflags "-X main.BuildVersion=a9694f8 \
-X main.BuildTime=2018-06-20T19:35:18-04:00"
./bin/foo version
web version 4b6c176 ("2017-05-12T17:59:34-04:00")

This application now has the Git SHA and the time it was built embedded in the binary, making it easy to diagnose exactly what, and when, the binary was built.

Modern Build Features

Reproducible Builds with -trimpath

By default, Go embeds full file paths in binaries for debugging. This means binaries built on different machines will differ. Use -trimpath to remove these paths:

$ go build -trimpath -o bin/myapp

This is especially useful for CI/CD pipelines and when distributing binaries.

Go Version Constraints

You can restrict code to specific Go versions using build constraints:

//go:build go1.21

package main

This file will only be compiled when using Go 1.21 or later. This is useful when using features from newer Go versions while maintaining backward compatibility.

Simple Commit Script

Given the following code:


package main

import (
	"fmt"
	"time"
)

var buildTime string
var sha1 string = "-"

func init() {
	buildTime = time.Now().Format("2006-01-02_15:04:05")
}

func main() {
	fmt.Printf("buildTime: %s\n", buildTime)
	fmt.Printf("sha1: %s\n", sha1)
}

You can embed the build time and sha1 commit with the following one liner:

now=$(date +'%Y-%m-%d_%T') go build -ldflags "-X main.sha1=`git rev-parse HEAD` -X main.buildTime=$now" -o bin/hello

Running it will now have the build time and commit sha1:

$ ./bin/hello
buildTime: 2020-12-17_11:08:18
sha1: d70dbac82d3e81a232a51f1e4d94000ca41c5df5

Exercise (10 Minutes)

Give the following code:


package main

import "fmt"

var version = "development"

func main() {
	fmt.Printf("Version: %s\n", version)
}

// TODO Add a //go:build tag so this only compiles for your operating system

package main

import "fmt"

func init() {
	// TODO Change this message to print out the operating system you are working on (just change the text message)
	fmt.Println("This runs on a specific operating system...")
}
  • Compile the program for your local operating system
    • specify the binary to be called exercise or exercise.exe depending on your operating system.
    • Run the compiled binary.
  • If you are on a windows computer, compile it for a Mac OS, if you are on a Mac/Linux, compile it for a Windows computer.
  • Compile with the -ldflags and change the version to 1.0.0 when the binary runs.
  • Change the init.go file to only build with your target operating system by adding a //go:build tag

Solution

  • Compile the program for your local operating system
    • specify the binary to be called exercise or exercise.exe depending on your operating system.
    • Run the compiled binary.
$ go build -o exercise
$ ./exercise
This runs on a darwin operating system...
Version: development
  • If you are on a windows computer, compile it for a Mac OS, if you are on a Mac/Linux, compile it for a Windows computer.

Mac OS:

GOOS=darwin go build

Windows:

GOOS=windows go build
  • Compile with the -ldflags and change the version to 1.0.0 when the binary runs.
$ go build -ldflags "-X main.version=1.0.0" -o exercise
$ ./exercise
This runs on a darwin operating system...
Version: 1.0.0
  • Change the init.go file to only build with your target operating system by adding a //go:build tag

//go:build darwin

package main

import "fmt"

func init() {
	// TODO Change this message to print out the operating system you are working on (just change the text message)
	fmt.Println("This runs on a darwin operating system...")
}