go build

Learn how to compile Go programs into standalone executables with go build, including cross-compilation, build flags, and build constraints.

The go build command turns your Go source code into a finished, runnable binary file. Where go run compiles and executes your program in one short‑lived step, go build creates a permanent artifact you can distribute, deploy, or run without the Go toolchain being present.

How go build Works

When you issue go build inside a directory that holds a main package, the Go compiler processes every .go file in that package along with all their imported dependencies, and then links them into a single executable. The binary is placed in your current working directory.

If you run go build on a package that does not declare package main, the command still compiles and type‑checks the code, but no executable is produced. This is an important difference that new Go developers often misunderstand: only main packages generate runnable binaries.

Binary Naming Convention

The name of the generated binary follows a predictable rule:

  • It is the name of the directory that contains the main package.
  • On Windows, an .exe extension is appended automatically.

The naming convention is consistent across platforms, but the exact form you see in your terminal depends on your operating system.

$ pwd
/home/user/projects/hello
$ go build
$ ls
hello  main.go
$ ./hello
Hello, World!

If you want a different output name, use the -o flag.

go build -o myapp

This creates myapp (or myapp.exe on Windows) regardless of the directory name.

Binary ready:

If you see the executable appear in your directory and it runs without errors, the build worked correctly.

Why go build Exists

A Go program does not need the Go toolchain to run. Once compiled, it is a self‑contained binary that includes the Go runtime and all necessary dependencies. This means you can copy the file to a machine that has no Go installed and it will still execute.

go run hides this reality. It compiles your code into a temporary file, executes it, and then deletes the binary when the program exits. That is fine for quick tests, but unsuitable for anything beyond development. When you use go build:

  • You get a persistent artifact that you can store, version, and ship.
  • You avoid re‑compiling the same code over and over, which reduces startup time.
  • You never accidentally run a stale or unintended version because you re‑run the same binary.
  • You can attach metadata (like build versions) via -ldflags, which go run makes difficult.

Avoid go run in production:

Using go run in production containers leaves the Go toolchain inside the image, increases attack surface, and makes every restart recompile the application. Use go build to produce a lean, reproducible binary.

The Mental Model for Beginners

Think of go build as a factory machine. You feed it blueprints (.go files) and it outputs a finished product (the binary). The machine is the Go compiler and linker. The blueprints are your source code plus all the packages you import. Once the product comes out, you no longer need the machine — you just use the product.

You also do not need to understand exactly how the machine works internally. But you do need to know the levers you can pull: which file to name the output, what operating system to build for, and which optional code sections to include.

Common Build Flags

-o — Custom Output Name

Override the default binary name and location.

go build -o bin/my-server

This places the executable in the bin/ directory, named my-server. The output path is relative to the current directory.

-v — Verbose Output

Show which packages are being compiled as the build proceeds.

go build -v

You will see lines like:

internal/unsafeheader
internal/race
...
yourproject/hello

This is useful for debugging build issues or understanding the dependency graph.

-x — Print Commands

Display every underlying command the Go toolchain runs — compiler invocations, linker flags, temporary directory creation.

go build -x

The output is verbose and low‑level. It helps when you suspect the toolchain is picking up unexpected files or when you need to see which environment variables (like CGO_ENABLED) are actually in use.

-ldflags — Linker Flags

The linker combines compiled packages into the final binary. The -ldflags flag lets you pass options to the linker. The most common use is to set string variables in your code at build time.

go build -ldflags "-X main.version=1.2.3" -o myapp

In your main package, declare a variable that will hold this value:

package main
import "fmt"
var version = "dev"
func main() {
    fmt.Println("Version:", version)
}

At runtime, version will be "1.2.3" instead of "dev". This is the standard technique for embedding build metadata (commit hash, build time) into a binary.

Source of truth:

The variable you set with -X must exist in the package you specify. It cannot be an unexported (lowercase) variable — the linker cannot see it.

A real‑world pattern pulls the current Git commit hash:

go build -ldflags "-X main.commitHash=$(git rev-list -1 HEAD)" -o myapp

Now your logs can include the exact commit the binary was built from, which is invaluable during debugging.

No spaces in ldflags values:

If the value contains spaces, wrap the entire -ldflags argument in single or double quotes accordingly. A missing quote can silently produce an incorrect build.

Cross‑Compilation with GOOS and GOARCH

Go can compile a binary for a different operating system or CPU architecture than the one you are currently using. You control this with two environment variables:

  • GOOS — the target operating system (linux, darwin, windows, etc.)
  • GOARCH — the target architecture (amd64, arm64, 386, etc.)

Set them before running go build:

GOOS=linux GOARCH=amd64 go build -o myapp-linux

This produces an amd64 Linux binary, even if you are on a macOS or Windows machine. The Go toolchain knows how to emit machine code for the target without any additional configuration, as long as you are using only pure Go packages.

CGO and cross-compilation:

If any package you depend on uses CGO (C code), cross‑compilation becomes much harder. The compiler then needs a cross‑compilation toolchain for the target platform. Unless you absolutely need CGO, set CGO_ENABLED=0 to keep builds portable:

CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o myapp-linux-arm64

This disables CGO and ensures your binary is pure Go.

A single command can target multiple platforms sequentially. For a small project, a Makefile or script often combines several builds:

# Build for three platforms
GOOS=linux GOARCH=amd64 go build -o bin/myapp-linux-amd64
GOOS=darwin GOARCH=arm64 go build -o bin/myapp-darwin-arm64
GOOS=windows GOARCH=amd64 go build -o bin/myapp-windows-amd64.exe

This is how a single codebase produces separate binaries for Linux servers, Apple Silicon Macs, and Windows machines — all without changing a line of source code.

Build Constraints and Tags

You may need certain parts of your code to exist only under specific conditions: a particular operating system, a certain hardware capability, or a feature you want to optionally include. Go offers two complementary mechanisms for this: file‑level naming conventions and explicit build tags.

File Name Suffixes

If a file ends with _GOOS.go or _GOARCH.go (or both), Go only compiles that file when the target matches. For example:

// config_linux.go
package main
func platform() string {
    return "linux"
}
// config_windows.go
package main
func platform() string {
    return "windows"
}

When you build for GOOS=linux, only config_linux.go is included; when you build for GOOS=windows, only config_windows.go is used.

Build Tags

A more explicit approach places a //go:build directive at the very top of a file (before the package declaration, followed by a blank line). This is called a build constraint. You can then enable the file by passing the -tags flag to go build.

Suppose you have a features_pro.go file that you only want to include when building a paid tier:

//go:build pro
package features
import "fmt"
func ExtraFeature() {
    fmt.Println("Pro feature active")
}

A normal build ignores this file:

go build ./...

To include it, use:

go build -tags pro

You can combine multiple tags. The syntax supports &&, ||, and !:

//go:build (linux || darwin) && !android

This includes the file only on Linux or macOS, but not on Android.

Order matters:

The //go:build line must appear at the top of the file, with no blank line before it, and be followed by a blank line. If you place it in the middle of a file, the constraint is silently ignored — the file will be compiled as if the constraint does not exist.

File suffixes and build tags can coexist. A file named helper_linux.go with a //go:build pro tag will only be compiled when the target is Linux and the pro tag is active.

What Happens If You Build a Non‑main Package

When you run go build in a directory where the package is not main, the command compiles everything to check for errors, but discards the result — no binary appears. This is by design, because a library package cannot be turned into a standalone executable.

If you want to verify that a library compiles cleanly, you can use:

go build ./...

This builds all packages in the current module without producing binaries for the libraries. It is a fast way to catch compilation errors across an entire project.

Build caching:

Even when no binary is produced, go build populates the build cache. Subsequent builds of unchanged packages are nearly instant.

Common Pitfalls

Expecting a binary from a non‑main package. If you are inside a lib/ directory that declares package lib, running go build yields no output file. You must be in a main package to get an executable.

Cross‑compiling with CGO enabled. Without a cross‑compilation C toolchain, the build will fail. Either disable CGO with CGO_ENABLED=0 or use a native build environment.

Forgetting file name suffixes when adding platform‑specific code. A file named config_windows.go will be ignored when building for Linux, even if you forget that it exists. This can lead to missing functions at link time. Always test a build for each target platform.

Confusing go run flags with go build flags. Flags like -tags and -ldflags work with both commands, but -o does not exist for go run. Build the binary when you need a specific output path.

Using go build without specifying a module root. If you are outside a Go module and your code has imports, the build will fail with an error about missing dependencies. Initialize a module with go mod init first.

Summary

The go build command is the gateway from source code to a deployable artifact. It does more than just compile — it lets you name the output, target different platforms, control which code sections are included, and stamp the binary with build information. Each of these capabilities is accessible through a handful of consistent flags.

The most important takeaway is not the flags themselves, but the design behind them: Go treats a build as a deterministic, repeatable process. The same source, with the same environment and tags, always produces the same binary. That property is what makes Go’s build system reliable for everything from a small CLI tool to a multi‑service backend deployed across mixed architectures.

If you understand binary naming, cross‑compilation, build tags, and -ldflags, you have the essential toolbox to build Go programs for any target.