From source code to standalone binary
You just finished a CLI tool that works perfectly on your machine. Now you need to ship it to a production server, hand it to a teammate who does not use Go, or bake it into a container image. go run is fantastic for rapid iteration, but it compiles to a temporary file and deletes it when the process exits. You need a persistent artifact. That is what go build does. It takes your source tree, resolves every dependency, compiles the code, and links it into a single standalone executable. The resulting file carries your logic, the standard library, and every third-party module it needs. You can copy it to a bare Linux box or a Windows desktop, and it runs without a Go installation.
Think of go build as an automated assembly line. You provide the blueprints. The toolchain fetches raw materials from the module supply chain, compiles each package into object files, and feeds them to the linker. The linker stitches everything together, resolves memory addresses, and outputs a single binary. The process is deterministic. If you run the exact same command with the exact same source code and dependencies, you get the exact same binary. Go enforces this through a content-addressed build cache. The compiler hashes your source files, the dependency versions, the compiler version, and every flag you pass. If the hash matches a cached result, the build returns instantly. You rarely need to clear the cache. Trust the hash.
How the build pipeline actually works
Before you run the command, the toolchain reads your go.mod file. It downloads missing modules, verifies checksums, and locks dependency versions. This step ensures reproducibility. Once dependencies are resolved, the compiler processes each package in dependency order. It translates Go source code into assembly, then into object files. The linker takes all object files, resolves cross-package references, and produces the final executable. The entire pipeline runs in parallel when possible. Large projects compile quickly because independent packages build simultaneously.
The build cache lives in your home directory under a hidden go folder. It stores compiled object files and the final binaries. The cache key includes the source code, the Go version, the target OS and architecture, and every build flag. Change a single character in a file, and the cache invalidates only the affected package. Everything else reuses cached artifacts. This design makes iterative development feel instant. You do not need to manage temporary files or clean build directories manually. The toolchain handles it. Let the cache do the heavy lifting.
The minimal build
Here is the baseline workflow. Write a main package, run the command, and get an executable named after your directory.
// main.go
package main
import "fmt"
// main is the entry point for the executable.
func main() {
// Print a greeting to standard output.
fmt.Println("Hello from the compiled binary")
}
# Compile the current directory into an executable.
# The output file inherits the directory name.
go build
Run this inside a folder called myapp, and the toolchain produces myapp (or myapp.exe on Windows). Execute it, and it prints the message. The binary contains everything it needs to run. The build cache stores the compiled object files in your home directory. Subsequent runs skip compilation entirely unless you modify a file or change a dependency. The cache is fast. Let it do the heavy lifting.
Controlling the output
Real projects require more control than a default build provides. You will want custom output names, stripped debug symbols, and injected metadata. go build accepts flags to tune the linker and compiler behavior. The -o flag sets the output path. The -v flag prints each package as it compiles. The -race flag enables the data race detector. The -tags flag filters files based on build constraints.
Here is a production-ready build command that handles naming and size optimization.
# Build with a custom name and strip debug metadata.
# -o sets the output file path.
# -ldflags passes arguments directly to the linker.
# -s removes the symbol table. -w removes DWARF debug info.
go build -o myapp -ldflags="-s -w"
The -ldflags option bridges the gap between build-time configuration and runtime behavior. The -s flag drops the symbol table. The -w flag removes DWARF debugging information. Together they shrink the binary significantly. You lose the ability to step through the code with a debugger, but the file transfers faster and consumes less disk space. This is standard practice for release builds. Strip the symbols. Ship the lean binary.
Injecting version info
Hardcoding version strings in source code creates friction. You update the string, commit, build, and hope you remembered. A cleaner approach injects the version at compile time using the -X linker flag.
// main.go
package main
import "fmt"
// Version holds the build version injected by the linker.
var Version = "dev"
// main prints the version string to verify injection.
func main() {
// Output the version to standard output.
fmt.Println("Version:", Version)
}
# Build and replace the Version variable at link time.
# -X expects the format importpath.name=value.
go build -o myapp -ldflags="-X main.Version=v1.2.3"
Run the binary, and it prints Version: v1.2.3. The source code still contains dev, but the compiled artifact carries the injected value. This pattern lets your CI pipeline control metadata without touching the repository. When you write the code that ends up in this binary, remember the error handling convention. if err != nil { return err } is verbose by design. The boilerplate makes the unhappy path visible. Don't hide errors. The binary will run, but it will fail silently if you ignore them.
Cross-compilation
Go makes cross-compilation trivial. You can build a binary for a different operating system or architecture without installing cross-compilers or toolchains. The standard library contains platform-specific code, and the compiler knows how to swap it out automatically.
Set the GOOS and GOARCH environment variables before running go build.
# Build a Linux binary on macOS.
# GOOS sets the target operating system.
# GOARCH sets the target architecture.
GOOS=linux GOARCH=amd64 go build -o myapp-linux
The toolchain downloads the necessary standard library sources for the target platform and compiles against them. The result is a binary that runs on Linux, even though you built it on macOS. This is how you ship binaries for multiple platforms from a single developer machine. Build once for linux/amd64, once for linux/arm64, once for darwin/amd64, and once for windows/amd64. You get a set of binaries ready for distribution. Cross-compilation removes the need for complex build matrices. Set the variables. Run the build.
Pitfalls and compiler behavior
go build behaves differently than you might expect if you are coming from interpreted languages. The command only produces an executable if the package is named main. If you run it inside a directory containing package mylib, the command succeeds but produces no output file. It compiled the package into the build cache and stopped. To get a binary, you must target a main package explicitly.
Build tags filter files before compilation begins. The modern syntax uses //go:build at the top of the file. This comment tells the compiler whether to include the file in the current build.
// linux_only.go
//go:build linux
package main
// setupLinux runs only on Linux systems.
func setupLinux() {
// Linux-specific initialization logic.
}
If you are on macOS and try to build this file, the compiler ignores it. The function setupLinux will not exist in the binary. If you reference that function elsewhere without a matching tag, the compiler rejects the program with undefined: setupLinux. Build tags are strict filters. Use them to separate platform-specific code or to exclude heavy integration tests from normal builds. The tag integration is a common convention for tests that require external services. Run go test -tags=integration to include them.
The race detector adds significant overhead. When you use -race, the binary runs slower and consumes more memory. It instruments memory accesses to detect concurrent reads and writes to the same variable. Use it for testing, not for production. The binary built with -race is not suitable for high-performance workloads.
go build does not run tests. It compiles the code. If you want to execute your test suite, use go test. If you want to check for suspicious constructs, use go vet. go build is strictly about producing the artifact. When you write functions that end up in the binary, follow the receiver naming convention. The receiver name is usually one or two letters matching the type: (b *Buffer) Write(...), not (this *Buffer) or (self *Buffer). This keeps method signatures readable. Public names start with a capital letter. Private names start lowercase. go build enforces this at the package boundary. If you export a function, it starts with a capital. If you keep it internal, use lowercase. The compiler won't let you export a lowercase name. Don't pass a *string. Strings are already cheap to pass by value. Passing a pointer to a string adds indirection without saving memory. The compiler doesn't stop you, but the community frowns on it. Goroutines are cheap. Channels are not magic. If your binary spawns goroutines, ensure they have a cancellation path. A goroutine leak happens when a goroutine waits on a channel that never closes. Always close channels or use context cancellation. The worst goroutine bug is the one that never logs. Trust gofmt. Argue logic, not formatting. go build doesn't format your code. Run gofmt -w . to fix indentation and spacing. Most editors run this on save. The community accepts the formatting style because it removes debates about whitespace.
Decision matrix
Use go build when you need a standalone executable to deploy or distribute.
Use go run when you are iterating on code and just want to test behavior quickly.
Use go install when you want to place the binary in your GOPATH/bin for global access.
Use go vet when you want to check for suspicious constructs without compiling.
Use go test when you need to run your test suite.
Use go build -race when you suspect a data race and need to detect concurrent memory access violations.
Use go build -tags=integration when you need to include files marked with specific build constraints.
Use GOOS and GOARCH with go build when you need to compile for a different platform than your current machine.