Go path · From source to binary · Topic 1 of 13

One binary, runtime included

Not started

go build compiles your packages ahead of time into machine code and links them, together with every standard-library package they import and the Go runtime, into a single executable. Nothing else has to be installed on the machine that runs it. Most of what makes Go feel different at run time, from cheap goroutines to the garbage collector, lives in that runtime, which is just a library linked into every program.

Why it matters: Knowing what is in the binary explains why Go containers can be tiny, why cross-compiling is one environment variable, and where goroutines and the collector come from.

  1. 1Compiled ahead of time

    The gc compiler turns each package into machine code and the linker joins them into one executable. There is no VM and no bytecode at run time.

  2. 2The runtime is linked in

    Every binary carries the Go runtime, which provides the scheduler, the garbage collector, stack management and maps. It is a library, not a separate process.

  3. 3Static by default

    Go code is linked statically. With cgo turned off on Linux the result needs no shared libraries at all, so it runs in an empty container.

  4. 4Cross-compiling is built in

    Set GOOS and GOARCH and the same toolchain builds for another operating system or CPU.

1

How it works

The explanation, with examples you can run.

What a build does

The go command finds the packages your main package imports, compiles each one (caching the results, so rebuilds are fast) and hands the object files to the linker. Imports are checked by the compiler: an unused import is an error, which keeps the dependency graph honest.

package main import (	"fmt"	"runtime") func main() {	fmt.Println(runtime.Version(), runtime.GOOS+"/"+runtime.GOARCH)	fmt.Println("CPUs:", runtime.NumCPU(), "GOMAXPROCS:", runtime.GOMAXPROCS(0))	fmt.Println("goroutines already running:", runtime.NumGoroutine())}

On an Apple-silicon Mac with Go 1.26 this printed go1.26.1 darwin/arm64, the CPU count twice, and one goroutine: the one running main.

The runtime is part of your program

Go's runtime plays roughly the part libc plays for C, but it does much more. It schedules goroutines onto operating-system threads, grows and shrinks their stacks, runs the concurrent garbage collector, implements maps and channels, and prints the stack traces you see on a panic. It starts before main and its background work runs on threads it manages. That is why a trivial program is a couple of megabytes: the FAQ points out that the binary also carries the type information needed for reflection and panics.

Static, mostly

The gc linker links Go code statically. Two things can pull in the system C library dynamically:

  • cgo. If cgo is enabled, some standard packages (DNS lookups in net, user lookups in os/user) may use the C versions. Building with CGO_ENABLED=0 uses the pure-Go ones.
  • The platform. macOS requires programs to go through libSystem, so Mac binaries always link it.
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app ./cmd/appfile app            # ELF 64-bit LSB executable, x86-64, statically linkedgo version -m app   # the Go version and module versions it was built from

A static Linux binary can be copied into a scratch or distroless container image with nothing else in it, apart from CA certificates if it makes TLS calls and time-zone data if it needs it (or import time/tzdata to embed it).

Cross-compiling

Because the compiler and linker are written in Go and the standard library has no C dependency when cgo is off, building for another target is just GOOS and GOARCH. go tool dist list prints every supported pair. Build tags and file suffixes such as _linux.go or _arm64.go select platform-specific files.

go run, go install, go vet

  • go run . builds into a temporary directory and runs the result: handy, but it is still a full compile.
  • go install puts the binary in $GOBIN (by default $HOME/go/bin).
  • go vet runs static checks that catch real bugs, such as Printf format mismatches, copied locks and Add called inside a WaitGroup goroutine. go test runs a subset of them automatically.
2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.What is in the executable that go build produces?
  2. 2.You want a Linux binary with no dependency on the system C library. What do you set?
  3. 3.How many goroutines does runtime.NumGoroutine() report at the start of main in a program that has started none?

0 of 3 right so far. Get all three to finish this topic.