ZetCode

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.

main.go
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.

mathx/mathx.go
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.

main.go
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.

msg_debug.go
//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.

msg_release.go
//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.

main.go
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.

main.go
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.

main.go
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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Go tutorials.