How to Use go tool link and go tool compile

Cli
Use go tool compile to create object files and go tool link to combine them into an executable binary.

When the black box needs to open

You have a Go program that builds perfectly with go build. Then you need to inject a custom flag into the linker to set a build version, or you're debugging a cryptic "undefined symbol" error that go build buries under layers of package resolution. The go command is a fantastic wrapper, but sometimes you need to see the gears turning. That's when you reach for go tool compile and go tool link. These are the raw engines behind every Go binary.

Concept: Rooms and assembly

Think of building a house. go tool compile is the factory that prefabricates individual rooms. You give it the blueprints (source code), and it spits out a finished room module (an object file). The room has walls and windows, but it doesn't have power or water yet. It has placeholders where the wiring should connect.

go tool link is the construction crew on site. It takes all the room modules, stacks them together, runs the wiring and pipes between them, and seals the foundation. It patches the placeholders so every switch connects to the right circuit. The result is a house you can actually live in (an executable).

Minimal example

Here's the bare minimum workflow. You write a file, compile it to an object, then link it to a binary.

// main.go
package main

// Print a greeting to stdout.
func main() {
    println("Hello from the raw tools")
}
# Compile source to object file
go tool compile -o main.o main.go

# Link object file to executable
go tool link -o myapp main.o
# Run the result
./myapp
# prints: Hello from the raw tools

Compile checks your logic. Link connects your world.

What happens under the hood

When you run go tool compile, the tool reads the .go file, parses the syntax, checks types, and generates machine code. It outputs an object file (.o on Unix, .obj on Windows). This file contains the compiled instructions for your package, but it has holes. If your code calls fmt.Println, the object file knows a function named fmt.Println exists, but it doesn't contain the code for it. It just has a placeholder symbol.

The linker fills those holes. go tool link takes the object file and finds the fmt package. It merges the fmt code into the binary and patches the placeholders so the call jumps to the right address. The linker also adds the Go runtime, the garbage collector, and the entry point. The output is a standalone binary that contains everything it needs to run.

The linker expects a main package. If you try to link an object file from a library package, the linker rejects it with no main packages or an error about missing entry points. You must compile a package named main with a main function to produce an executable.

Run gofmt on your code before compiling. The community uses a single formatting style. Most editors run gofmt on save. Don't argue about indentation; let the tool decide.

Realistic example: injecting build metadata

Real projects rarely consist of a single file. More often, you need to pass metadata to the binary without changing the source code. Build systems use linker flags to inject version numbers, commit hashes, or build timestamps. The -X flag lets you set the value of a string variable during the link step.

// main.go
package main

// Version gets injected at link time.
var Version = "dev"

func main() {
    println("App version:", Version)
}
# Compile as usual
go tool compile -o main.o main.go

# Link with a flag to override the Version variable
go tool link -o myapp -X main.Version=1.0.0 main.o
./myapp
# prints: App version: 1.0.0

The -X flag takes the form importpath.name=value. The path must match the package where the variable lives. This technique keeps version info out of your source tree and lets the build system control it.

Linker flags are the bridge between build time and runtime.

Pitfalls and errors

The raw tools give you less hand-holding than go build. If you forget to compile a dependency, the linker complains with undefined: package.function. The error points to the missing symbol, not the missing file. You have to figure out which object file should have provided it.

Object files are tied to the Go version and architecture. You can't link an object file compiled with Go 1.20 into a binary linked with Go 1.22. The linker will reject it with an invalid object file error. Always compile and link with the same toolchain.

If you reference a variable that doesn't exist, the compiler rejects the program with undefined: x. If you import a package and don't use it, you get imported and not used. The compiler enforces strict hygiene. You can't have dead imports cluttering the namespace.

Names starting with a capital letter are exported. Names starting lowercase are private to the package. The compiler enforces this boundary. You can't link against a lowercase symbol from another package. If you need to expose a value, capitalize it. If you don't, keep it lowercase.

The linker doesn't guess. If a symbol is missing, you provide it or you fix the code.

When to use what

Use go build when you are developing an application or library. It handles dependencies, caching, and testing automatically. It is the standard tool for 99% of workflows.

Use go tool compile when you need to inspect the object file or debug a compilation error that go build obscures. It isolates the compilation step so you can focus on a single package.

Use go tool link when you need to pass custom linker flags like -X for version injection or -s to strip debug symbols. It gives you direct control over the final binary layout.

Use go tool compile and go tool link together when you are writing a custom build tool or a CI script that requires fine-grained control over the build process.

Trust the go command for daily work. Reach for the tools when the wrapper gets in the way.

Where to go next