When developing Go packages that rely on specific features of the underlying platform or processor it is often necessary to provide a specialised implementation.
Go does not have a preprocessor, a macro system, or a #define
declaration to control the inclusion of platform specific code. Instead a system of tags and naming convention defined in the go/build
package and supported by the go
tool allows Go packages to customise themselves for the specific platform they are being compiled for.
This post explains how conditional compilation is implemented and show you how you can use it in your projects.
But first, go list
Before we can talk about conditional compilation, we need to learn a little bit about the go list
command. go list
gives you access to the internal data structures which power the build process.
go list
takes the most of the same arguments as go build
, test
, and install
but does not perform any compilation. Using the -f
, format flag we can supply a snippet of text/template
code which is executed in a context containing a go/build.Package
structure.
Using the format flag, we can ask go list
to tell us the names of the files that would be compiled.
% go list -f '{{.GoFiles}}' os/exec
[exec.go lp_unix.go]
In the example above I asked for the list of files in os/exec
package that would be compiled on this linux/arm
system. The result is two files, exec.go
which contains the common code shared across all platforms, and lp_unix.go
while contains an implementation of exec.LookPath
for unix-like systems.
If I were to run the same command on a Windows system, the result would be
C:\go> go list -f '{{.GoFiles}}' os/exec
[exec.go lp_windows.go]
This short example demonstrates the two parts of the Go conditional compilation system, known as Build Constraints, which we will now explore in more detail.
Build tags
The first method of conditional compilation is via an annotation in the source code, commonly known as a build tag.
Build tags are implemented as comments and should appear as close to the top of the file as possible.
When go build
is asked to build a package it will analyse each source file in the package looking for build tags. These tags control whether go build
will pass the file to the compiler.
A build tags follow these three rules
- a build tag is evaluated as the OR of space-separated options
- each option evaluates as the AND of its comma-separated terms
- each term is an alphanumeric word or, preceded by !, its negation
As an example, the build tag found at the top of a source file
// +build darwin freebsd netbsd openbsd
would constrain this file to only building on BSD systems that supported kqueue.
A file may have multiple build tags. The overall constraint is the logical AND of the individual constraints. For example
// +build linux darwin
// +build 386
constrains the build to linux/386
or darwin/386
platforms only.
A note about comments
One thing that generally catches people out when they are first trying to make build tags work is this
// +build !linux
package mypkg // wrong
In this example there is no newline separating the build tag and the package declaration. Because of this the build tag is associated with the package declaration as a comment describing the package and thus ignored.
// +build !linux
package mypkg // correct
This is the correct form, a comment with a trailing newline stands alone and is not associated with any declaration and go vet
will detect the missing newline.
% go vet mypkg
mypkg.go:1: +build comment appears too late in file
exit status 1
When this feature was added to go vet
it detected several mistakes in the standard library and sub repos, so don’t feel bad if you get it wrong the first time.
For reference, here is a sample showing a licence preamble, a build tag, and a package declaration
% head headspin.go
// Copyright 2013 Way out enterprises. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build someos someotheros thirdos,!amd64
// Package headspin implements calculates numbers so large
// they will make your head spin.
package headspin
File suffixes
The second option for providing conditional compilation is the name of the source file itself. This scheme is simpler than build tags, and allows the go/build
package to exclude files without having to process the file.
The naming convention is described in the documentation for the go/build
package. Simply put, if your source file includes the suffix, _$GOOS.go
then it will only be built on that platform. All other platforms will behave as if the file is not present. The same applies for _$GOARCH.go
. The two can be combined as _$GOOS_$GOARCH.go
, but not _$GOARCH_$GOOS.go
.
Some examples of file suffixes are,
mypkg_freebsd_arm.go // only builds on freebsd/arm systems
mypkg_plan9.go // only builds on plan9
Your source files still require a name, a suffix is not sufficient, for example
_linux.go
_freebsd_386.go
will be ignored, even on linux or freebsd systems, because the go/build
package ignores any file beginning with a period or an underscore.
Choosing between build tags and file suffixes
Build tags and file suffixes overlap in fuctionality. For example, a file called mypkg_linux.go
that contained the build tag // +build linux
is redundant.
In general, when choosing between a build tag or a file suffix, you should choose a file suffix when there is an exact match between the platform or architecture and the file you want to include. eg,
mypkg_linux.go // only builds on linux systems
mypkg_windows_amd64.go // only builds on windows 64bit platforms
Conversely if your file is applicable to more than one platform or architecture, or you need to exclude a specific platform, a build tag should be used. eg,
% grep '+build' $HOME/go/src/pkg/os/exec/lp_unix.go
// +build darwin dragonfly freebsd linux netbsd openbsd
builds on all unix like platforms.
% grep '+build' $HOME/go/src/pkg/os/types_notwin.go
// +build !windows
builds on all platforms except Windows.
Wrapping up
While this post has focused only on Go source files, build tags and file suffixes can be used with any source file that the go
tool can build. This includes .c
and .s
files. The Go standard library, specifically the runtime
, syscall
, os
and net
packages contain great examples, I recommend studying them.
Test files also support build tags and file suffixes and behave in the same manner as Go source files, conditionally including test cases on a per platform basis. Again the standard library contains many great examples.
Finally, while the title of this article talks about the go
tool, the conditional compilation features are not limited to just that tool. You can build your own tools to consume and analyse Go code with the same file suffix and build tag semantics using the go/build
package.