Compile time options in Go applications
Making software features and functionality configurable improves the quality
of both the code base and the end product. This can aid in debugging, increase
code maintainability, and make code more testable. Sometimes, however, you may
not want configuration parameters to change after compilation. Many programming
languages provide patterns for this. Let’s take a look at a simple pattern for
compile time options in Go applications using build tags and the cmd/
project structure.
Table of contents
Security concerns
Before we get started, it should be noted that some people may look at this and get the idea to compile credentials or secrets into their code.
Do not put secrets in your source code. If you are compiling secrets into your code, you are doing it wrong.
Lastly, this pattern will not stop someone from reading or editing compiled code. This pattern is meant to help prevent configuration mistakes. It will not stop or even deter reverse engineering efforts.
Build tags
Build tags (or build
constraints) provide a way to specify which Go source files will be compiled.
You have probably seen a few in source filenames, such as linux
and windows
.
Build tags can be specified by writing a special comment before the package
clause inside a source file. The following snippet contains the first three
lines of a source file included only in builds for Linux operating systems:
// +build linux
package main
It is very important that an empty new line follow the build tag comment. Otherwise the build tag(s) will be considered a normal comment, and will be ignored. The following snippet demonstrates the incorrect usage of the build tag comment:
// THIS IS WRONG - There must be an empty new line between the lines below.
// +build linux
package main
A magic comment will likely annoy some people - but it is what it is. There are
also a few magic build tags, such as operating system names. This is covered
in the documentation linked above. Build tags can be targeted at compile time
using the -tags
command line argument for go build
or go run
.
Leveraging build tags
Custom build tags can be specified by simply adding them in the build tags
comment. Let’s pretend that we need to define a function that returns a bool
which relates to the specified build tag. The following snippet demonstrates
this by creating a tag named debug
:
// +build debug
package myapp
func DisableMTLS() bool {
return true
}
The above code snippet demonstrates how to define a tag named debug
and
include a function named DisableMTLS
that returns true. Now, we need
a non-debug implementation of this function. How about we use the build tag
release
to differentiate it?
// +build !debug release
package myapp
func DisableMTLS() bool {
return false
}
The above file will be included when any tag besides debug
is specified, or
when release
is specified. This allows us to continue compiling the
application without specifying any build tags. Thus, the release
tag acts as
a sensible default, and should stop us from accidentally compiling a debug
application variant.
Lastly, we can call this function from our application:
package main
import (
"fmt"
"github.com/stephen-fox/myapp"
)
func main() {
fmt.Printf("DisableMTLS is %v\n", myapp.DisableMTLS())
}
Let’s take a look at what specifying different build tags does:
go run main.go
# Prints "DisableMTLS is false".
go run -tags release main.go
# Prints "DisableMTLS is false".
go run -tags debug main.go
# Prints "DisableMTLS is true".
Pretty neat! But I think we can do better.
Integrating with the cmd/
project structure
In the previous example, we implemented compile time options for a command line application using Go’s build tags. The compile options' source files were intertwined with other code, and were not clearly separated. Not exactly great for maintainability.
We can improve this by moving the compile time options into a separate Go
package containing only compile time options. Since these options are meant for
an application, and not a library, we should place the package near the main
package. Personally, I am a big fan of the
unofficial project layout,
which designates the cmd/
directory for application-specific code.
Here is what our new project structure looks like (this example project can be found on my GitHub):
myapp/
|-- doc.go
|-- go.mod
|-- library.go
|-- cmd/
|-- myapp/
|-- main.go
|-- compileoptions/
|-- doc.go
|-- options_debug.go
|-- options_release.go
Now we can move our compile time options into the compileoptions
package, and
document how to use them in the doc.go
file. This project structure clearly
communicates how compile time options are implemented in the project.
This pattern also prevents us from accidentally using compileoptions
in both
the command line application, and in the library code. If library.go
imported
code from compileoptions
, and main.go
imported code from both
compileoptions
and myapp
, then compilation would fail due to an
import cycle.
Our code in main.go
will look like this:
package main
import (
"fmt"
"github.com/stephen-fox/myapp/cmd/myapp/compileoptions"
)
func main() {
fmt.Printf("DisableMTLS is %v\n", compileoptions.DisableMTLS())
}
This clearly documents where the compile time logic and configuration is stored.
Compilation remains simple; only one source file is needed when running
go build
or go run
:
go run cmd/myapp/main.go
go run -tags debug cmd/myapp/main.go
It should be noted that this example demonstrates only a small slice of what is possible with this pattern. Constants, structs, and other functionality can be selectively implemented with this pattern.
I hope you find this information helpful. Good night, and good luck.
Updates and corrections
- May 12, 2022 - Move sections around to be consistent with new post structure
- September 13, 2020 - Style references more closely to APA format. General changes for new blog theme
References
- golang.org. (n.d.). Package build.
- Kyle Quest et al.. (n.d.). project-layout.
- Stephen Fox Jr. (n.d.). go-compile-time-options.