The Linker — Interview¶
Twenty questions an interviewer might ask about the Go linker, with crisp, correct answers. Covers internal vs external linking, dead-code elimination, -ldflags, buildmodes, .gopclntab, reproducible builds, and the reflection/linker interaction.
1. What does the Go linker (cmd/link) do?¶
It takes the compiled object files/archives (goobj format), resolves symbols, eliminates dead code, lays out sections, applies relocations, generates runtime tables (.gopclntab), DWARF, and build info, and writes the final OS-native executable (ELF/Mach-O/PE).
2. Does Go use the system ld?¶
Not by default. Pure-Go programs use internal linking — cmd/link writes the executable itself, no system linker needed. That's why pure-Go cross-compilation needs no C toolchain.
3. When does Go switch to external linking?¶
When there's host (C) object code to combine: non-trivial cgo, or build modes that require the host linker — c-shared, c-archive, plugin, shared, and usually pie. Also when you pass -linkmode=external, -extld, or -extldflags.
4. What are the -linkmode values?¶
internal (Go's linker), external (host linker via gcc/clang), and auto (default — the linker decides based on inputs/buildmode).
5. Explain dead-code elimination.¶
Starting from roots (main.main, runtime entry, exported symbols), the linker marks every symbol reachable via relocations and discards the rest. Unused functions and unreachable methods are simply not written into the binary. It's implemented in deadcode.go as a mark-and-sweep over the symbol graph.
6. How does deadcode handle methods called through interfaces?¶
It correlates two things: types that are converted to an interface (so their method set could be dispatched dynamically) and interface methods actually called at reachable sites. A method is kept if its type is boxed into an interface and a matching interface method (name+signature) is invoked somewhere reachable. A method on a type never boxed and never called directly is dropped.
7. Why does reflection bloat binaries?¶
reflect.Value.MethodByName/Method can invoke methods by name at runtime, so the linker can't prove which are unused. When it sees those reflect symbols become reachable, it disables method pruning and keeps full method sets plus type metadata (names, fields, tags). That extra retained code and data inflates size.
8. What do -s and -w do?¶
-s omits the symbol table (and debug info); -w omits DWARF debug info. Together (-ldflags="-s -w") they shrink the binary, mostly by removing DWARF.
9. After -s -w, do panics still show function names?¶
Yes. Function names and line numbers in panics/runtime.Callers come from .gopclntab, which is runtime-required and not removed by -s -w. You only lose source-level debugger (DWARF) support.
10. What is .gopclntab?¶
The PC-line table: maps any program counter to its function, source file/line, stack frame and GC pointer-map info, and inlining tree. The runtime needs it for stack unwinding (panics), GC stack scanning, profiling, and runtime.FuncForPC.
11. Difference between .gopclntab and DWARF?¶
.gopclntab is a runtime structure (generated by pcln.go), required for the program to function correctly. DWARF (.debug_*, from dwarf.go) is debugger format used by dlv/gdb, not needed at runtime, removed by -w.
12. How do you set a version string at build time?¶
-ldflags="-X importpath.name=value". The target must be a package-level string var. Example: go build -ldflags="-X main.version=1.4.2". It's silently ignored on const, non-string, or wrong import path.
13. Why might -X "not work"?¶
Target is a const (compiler-folded, nothing to patch), it's not a string, the import path is wrong (short name instead of full path), or spaces in the value weren't quoted. Verify with go version -m app | grep ldflags.
14. What are the main -buildmode options?¶
exe (default), pie (position-independent, ASLR), c-archive (static C embed), c-shared (.so/.dll for FFI), shared, plugin (runtime-loaded .so, Linux/macOS only), archive.
15. What's tricky about -buildmode=plugin?¶
Plugins require the host and plugin to be built with the exact same Go version, the same versions of all shared dependencies, the same GOOS/GOARCH, and compatible flags. Any skew → plugin was built with a different version of package .... Plugins also can't be unloaded. Many teams prefer subprocess+RPC.
16. What is -trimpath and why use it?¶
It strips absolute build-machine paths (e.g. /home/ci/...) from the embedded pclntab/buildinfo, replacing them with module-relative paths. It improves privacy and is required for reproducible builds (byte-identical across machines/directories).
17. How do you make a Go build reproducible?¶
-trimpath, optionally -ldflags=-buildid= for byte-identity, a pinned Go toolchain (GOTOOLCHAIN/go directive), avoid embedding timestamps, and a clean VCS checkout. Verify with sha256sum of two independent builds.
18. How is build/version info embedded since Go 1.18?¶
The toolchain auto-stamps VCS info (vcs.revision, vcs.time, vcs.modified) plus module and dependency versions into .go.buildinfo. Read it with go version -m <binary>, runtime/debug.ReadBuildInfo(), or the debug/buildinfo package — often replacing manual -X for commit hashes.
19. Static vs dynamic linking with cgo — what's the gotcha?¶
Pure-Go (CGO_ENABLED=0) is statically linked and scratch-friendly. With cgo on (default natively), you may get a dynamically linked binary needing libc — which breaks on Alpine (musl vs glibc) and scratch. Use CGO_ENABLED=0, or force static with -ldflags='-linkmode=external -extldflags=-static' (needs static C libs).
20. What tools inspect a linked binary?¶
go tool nm (symbols, -size -sort size for size ranking), go tool objdump (disassembly), go version -m (build info), readelf/otool/dumpbin (native sections), bloaty (size breakdown), and -ldflags=-dumpdep (deadcode dependency edges).
Rapid-fire¶
-extldflagsgoes to whom? The host (external) linker.- Does
-s -wremove.gopclntab? No. - Can
-Xset anint? No — strings only. - Default linkmode for pure-Go exe? Internal.
- Plugin on Windows? Not supported.
- What disables auto VCS stamping?
-buildvcs=false. - What keeps a reflected-only method alive reliably? Boxing the type into an interface that declares the method (
var _ I = T{}).