Go build
last modified October 30, 2026
In this article we show how to build executables with the go build
command. The Go toolchain compiles a package and its dependencies, and either
writes an executable or checks that the package can be built.
The go tool manages Go source code and has many subcommands,
including build, run, test,
install and vet. The syntax of the build command is:
go build [-o output] [build flags] [packages]
Without arguments, go build builds the package in the current
directory. The command does not install the result; it leaves the executable
in the current directory.
All commands in this article were run with Go 1.27.0 on Linux/amd64.
A first build
We start with a new module. The module path becomes the import path of the package and the default name of the executable.
$ mkdir simple $ cd simple $ go mod init com.zetcode/simple go: creating new go.mod: module com.zetcode/simple $ cat go.mod module com.zetcode/simple go 1.27.0
The program
The go.mod file records the module path and the language version.
The program prints the Go version, the operating system and the architecture it
was compiled for.
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Println("Go: ", runtime.Version())
fmt.Println("OS: ", runtime.GOOS)
fmt.Println("Arch:", runtime.GOARCH)
}
go run compiles the program to a temporary file and executes it.
No executable is left behind.
$ go run . Go: go1.27.0 OS: linux Arch: amd64
Building the executable
go build writes the executable named after the module path, in
this case simple. Running it produces the same three lines, and
file shows that the result is a statically linked ELF binary,
because the program uses only the Go runtime and the fmt package.
$ go build $ ls go.mod main.go simple $ ./simple Go: go1.27.0 OS: linux Arch: amd64 $ file simple simple: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, ...
The output name and -o
The -o flag sets the output path. A file name replaces the default
name; a path ending in a slash puts the executable into that directory under
its default name. Missing parent directories are created. The default name
comes from the last element of the module path, so renaming the module changes
the name of the executable.
$ go build -o bin/ . $ ls bin simple $ go build -o app . $ ls app bin go.mod main.go simple $ sed -i 's|com.zetcode/simple|com.zetcode/first|' go.mod $ go build $ ls app bin first go.mod main.go simple $ ./first Go: go1.27.0 OS: linux Arch: amd64
Building a list of files
go build also accepts a list of .go files instead of
a package. The executable is then named after the first file, without the
suffix.
The file-list form ignores other files in the directory. It is useful for a quick single-file build, but a package build is less error-prone because it includes every file that belongs to the package.
$ go build main.go $ ./main Go: go1.27.0 OS: linux Arch: amd64
Packages that are not main
Building a library package compiles it and reports errors, but writes no
executable. The same happens with go build ./..., which checks
every package in the module.
package mathx
func Double(n int) int {
return 2 * n
}
The package belongs to a module, but it is not a main package and
has no main function.
Both commands exit with status 0 and create no files. On a continuous integration server this is a fast way to verify that all packages compile.
$ go build ./mathx $ ls go.mod mathx $ go build ./... $ ls go.mod mathx
Go build and go install
go install compiles the package and copies the executable into
$GOBIN, or into $GOPATH/bin when GOBIN is
not set.
go build is the better choice inside a project, because it keeps
the result next to the source. go install is meant for tools that
should be available on the PATH.
$ GOBIN=$PWD/gobin go install . $ ls gobin first $ ./gobin/first Go: go1.27.0 OS: linux Arch: amd64
Cross compilation
The target operating system and architecture are set with the
GOOS and GOARCH environment variables. Cross
compiling does not need a C compiler when the program does not use cgo. A
Windows target automatically gets the .exe suffix. Other
architectures keep the usual name, so -o should be used to avoid
overwriting the native binary. The ARM file runs on a 64-bit ARM machine, not
on the build host.
$ GOOS=windows GOARCH=amd64 go build $ file first.exe first.exe: PE32+ executable (console) x86-64, for MS Windows $ GOOS=linux GOARCH=arm64 go build -o first-arm64 $ file first-arm64 first-arm64: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, ...
Build tags
A build constraint, written as a //go:build line, includes or
excludes a file from the build. The -tags flag enables custom
tags. The example is a separate module, and the three files below are its whole
package.
package main
import "fmt"
func main() {
fmt.Println(message())
}
Two files provide the message function, and exactly one of them
is compiled depending on the debug tag.
//go:build debug
package main
func message() string {
return "debug build"
}
The release file declares the opposite constraint, so exactly one of the two files is compiled in any build.
//go:build !debug
package main
func message() string {
return "release build"
}
The constraint must appear before the package clause and be followed by a blank
line. Without the tag the release file is used; with -tags debug
the debug file is used instead.
$ go build -o app .
$ ./app
release build
$ go build -tags debug -o app-debug .
$ ./app-debug
debug build
$ go list -f '{{.GoFiles}}' .
[main.go msg_release.go]
$ go list -tags debug -f '{{.GoFiles}}' .
[main.go msg_debug.go]
Injecting the version
The linker can set the value of a string variable with
-ldflags "-X importpath.name=value". For the main package the
import path is simply main. This example is another small module
with a single variable and a main function.
package main
import (
"fmt"
)
var version = "dev"
func main() {
fmt.Println("version:", version)
}
The variable keeps its default value in a normal build. The flag replaces it at link time, without changing the source.
The linker only sets string variables that are declared at package level; constants cannot be changed this way.
$ go build -o app . $ ./app version: dev $ go build -ldflags "-X main.version=1.2.3" -o app . $ ./app version: 1.2.3
Stripping the binary
The -s and -w linker flags omit the symbol table and
the DWARF debugging information. The program still runs, but debuggers and
some profiling tools lose information.
The stripped binary is about 36 percent smaller. The exact sizes depend on the toolchain, the target and the code, but the difference is typical for a small program.
$ go build -trimpath -o app . $ go build -trimpath -ldflags "-s -w" -o app-stripped . $ stat -c '%n %s' app app-stripped app 2337734 app-stripped 1507488 $ file app-stripped app-stripped: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, ..., stripped
Reproducible paths with -trimpath
By default the binary records the absolute paths of the source files. The
-trimpath flag removes the local directory prefix, which makes
builds from different checkouts produce the same binary. The string found in
the first binary is /tmp/gobuild/version/main.go, the directory
where the program was built; with -trimpath only the import path
remains. The Go version and the build settings of a binary can be inspected
with go version -m.
$ go build -o app-plain .
$ strings app-plain | grep -c /tmp/gobuild/version
1
$ go build -trimpath -o app-trim .
$ strings app-trim | grep -c /tmp/gobuild/version
0
$ go version -m app-trim
app-trim: go1.27.0
path com.zetcode/version
mod com.zetcode/version (devel)
build -buildmode=exe
build -compiler=gc
build -trimpath=true
build CGO_ENABLED=1
build GOARCH=amd64
build GOOS=linux
build GOAMD64=v1
Checking with go vet
go build does not run go vet. A program with a wrong
Printf verb compiles and runs, but prints a diagnostic marker
instead of the value.
package main
import "fmt"
func main() {
count := "three"
fmt.Printf("count: %d\n", count)
}
The build succeeds because the call is type-correct; the mismatch is between
the format string and the argument. go vet reports it and exits
with status 1.
Running go vet ./... before go build catches this
class of mistakes early.
$ go build -o app . $ ./app count: %!d(string=three) $ go vet . main.go:8:24: fmt.Printf format %d has arg count of wrong type string
Static binaries
A program that imports net is linked against the system C library
when cgo is enabled. Setting CGO_ENABLED=0 produces a static
binary that does not depend on the libraries of the target system.
package main
import (
"fmt"
_ "net"
)
func main() {
fmt.Println("net package linked")
}
The blank import pulls the package into the binary without using its identifiers. The two builds differ in how the DNS resolver and the system calls are linked.
Static binaries are convenient in minimal containers because they need no shared libraries at all.
$ go build -o app-cgo .
$ ldd app-cgo
linux-vdso.so.1
libresolv.so.2 => /lib/x86_64-linux-gnu/libresolv.so.2
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
/lib64/ld-linux-x86-64.so.2
$ CGO_ENABLED=0 go build -o app-static .
$ ldd app-static
not a dynamic executable
$ ./app-static
net package linked
Source
Go command documentation
Compile and install the application
In this article we have worked with go build. We built a module
and a list of files, set the output name, checked library packages, compared
go build with go install, cross compiled for Windows
and ARM, used build tags, injected a version string, stripped and trimmed the
binary, ran go vet, and produced a static executable with
CGO_ENABLED=0.
Author
List all Go tutorials.