The Go 1.3 linker got less clever
One of our larger binaries spent enough time linking that I could switch windows, forget why, and begin an unrelated task. Go 1.3’s linker refactor made that interruption noticeably shorter.
The old linker represented symbols and their data using structures that encouraged many small allocations and expensive traversals. The 1.3 work reorganized that machinery around more compact symbol data and a cleaner division of the linking stages. The user-facing command did not change:
$ go build ./cmd/server
The wait did. On one clean build of our program I saw roughly:
Go 1.2.2 real 4.8s
Go 1.3 real 3.1s
This is not a controlled toolchain benchmark. Installed package archives, the filesystem, machine load, and exact source all affect it. Repeating alternating builds from the same installed state still showed enough difference that it was not merely optimism generated by a new version number.
Linking a Go program is substantial work. The linker reads object data for the program and imported packages, resolves symbol references, applies relocations, lays out executable sections, emits metadata used by reflection and the runtime, and writes the target executable. It also removes unreachable code and data. A statically linked Go binary may contain a great deal of runtime and type information, so representation overhead inside the linker matters.
The refactor is interesting because it improves a tool by making its internal model less elaborate. No language feature was needed. Existing builds became faster and used less memory because the implementation stopped turning every byte and symbol into quite so much bookkeeping.
I still avoid reading too much into final binary size. Importing a package can pull in initialization and reachable symbols, reflection can retain type metadata, and debug information contributes too. A smaller linker heap is not the same thing as a smaller output file.
Cross-compilation makes architecture-specific tool names visible in this toolchain. The compiler and linker remain separate programs selected for the target architecture, even though go build normally hides the sequence. When investigating a failure I use go build -x to print invoked commands rather than reconstructing them from folklore.
That output also shows whether the build uses installed archives from GOROOT/pkg and GOPATH/pkg or recompiles packages. Without it, I had occasionally blamed the linker for time actually spent rebuilding a dependency after a build-tag change.
Benchmark builds with and without the relevant installed archives separately. Archives installed for the standard library under GOROOT/pkg and for workspace packages under GOPATH/pkg can hide compilation work; release builders often expose linker time more clearly. Go 1.3 improved both my build and my attention span, though only one claim has numbers behind it.