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
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.
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!!"//go:build happy
// happy.go
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
// +buildwas used. Thegofmttool automatically converts the old syntax to the new//go:buildformat. 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
import "fmt"
func init() {
fmt.Println("happy.go")
}
var greeting = "It's so great to see you!!"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
import "fmt"
func init() {
fmt.Println("sad.go")
}
var greeting = "I'm so sorry to see you. :("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!!
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/ppc64aix/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/ppc64Run 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
import "fmt"
func init() {
fmt.Println("happy.go")
}
var greeting = "It's so great to see you!!"The sad.go file should be compiled when building for linux.
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)
}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)
}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)
}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)
}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...")
}// 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
exerciseorexercise.exedepending on your operating system. - Run the compiled binary.
- specify the binary to be called
- 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
-ldflagsand change the version to1.0.0when the binary runs. - Change the
init.gofile to only build with your target operating system by adding a//go:buildtag
Solution
- Compile the program for your local operating system
- specify the binary to be called
exerciseorexercise.exedepending on your operating system. - Run the compiled binary.
- specify the binary to be called
$ 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
-ldflagsand change the version to1.0.0when 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.gofile to only build with your target operating system by adding a//go:buildtag
//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...")
}