What does the Go linker emit for debuggers by default, and what do `-ldflags="-s -w"` strip?
answer
- two kinds of metadata, not one
- the debugger's copy and the runtime's copy
- panic traces survive the strip
- one flag drops types and line locations
- the other drops the symbol table
basics
~20 sBy default the Go linker writes DWARF debug information plus the symbol table into the binary, which is what lets a debugger resolve names, types and source lines. Linking with -ldflags="-w" drops the DWARF and -s drops the symbol table.
solid answer
~50 sA Go binary carries two distinct kinds of metadata. DWARF sections describe types, variable locations and the instruction-to-source-line mapping; that is what a debugger consumes to set a breakpoint on a line and print a struct. Separately the runtime carries its own program-counter line table, `pclntab`, which is what `panic` uses to print a symbolised stack trace. `-ldflags="-w"` omits the DWARF; `-ldflags="-s"` omits the symbol table. Both shrink the binary meaningfully, and both are common in release builds. The important consequence is asymmetric: a stripped binary still prints readable panic traces with function names and line numbers, because that comes from `pclntab` and not from DWARF — but you can no longer attach a debugger to it and inspect typed locals. If you may need to do that, keep an unstripped copy of the exact same build.
code
text · 9 lines# fully inspectable: DWARF present, nothing optimised away
go build -gcflags="all=-N -l" -o fixtool.debug ./cmd/fixtool
# optimised, still attachable: DWARF present
go build -o fixtool ./cmd/fixtool
# release: -w drops DWARF, -s drops the symbol table;
# panic tracebacks still print function names and line numbers
go build -ldflags="-s -w" -o fixtool.release ./cmd/fixtoolgo deeper
Know that a Go binary ships debug information by default and that -ldflags="-s -w" is the common way to make a release smaller. Be able to say that doing so is what stops a debugger from attaching usefully.
Explain the two separate data sets: DWARF for the debugger, and the runtime's own line table for tracebacks. Be ready to say precisely which flag removes which, and why a stripped Go binary still prints named frames on panic.
Show the operational call: what you keep for post-mortem work, where the unstripped artefact lives, and why a rebuild from the same commit is not a substitute for the artefact that crashed. Recognise -trimpath as a separate cause of missing sources.
Own it as a release policy. Decide once whether artefact size justifies stripping, make retention of the matching unstripped build automatic rather than heroic, and ensure the choice is uniform so nobody debugging an incident has to guess what their binary contains.
## Two independent sets of metadata It is easy to assume a binary is either "debuggable" or "stripped". A Go binary is not that simple, and the distinction explains a behaviour that surprises people the first time they see it. **DWARF.** DWARF is the standard debug-information format on the platforms Go targets, carried in dedicated sections of the executable. It describes the things only a debugger needs: the layout and names of your types, where each variable lives at each instruction (this register here, that stack offset there), the lexical scopes, and the mapping from machine instructions back to source files and lines. The Go toolchain emits it by default. Without it a debugger can single-step machine instructions but cannot tell you that the thing in a register is a `fixture` with a `Count` field. **The runtime's own tables.** Separately, the linker embeds a program-counter line table — `pclntab` — plus function metadata, because the *runtime itself* needs them. When a goroutine panics, the runtime walks the stack and prints function names, file names and line numbers, and it does that in a binary running on a machine with no debugger, no source and no symbol server. That data is not DWARF and is not optional: unwinding, `recover`, and stack growth all depend on it. ## What the two link flags do ``` go build -ldflags="-s -w" -o fixtool ./cmd/fixtool ``` - `-w` omits the DWARF debug information. - `-s` omits the symbol table. They are usually written together in release builds because both shrink the artefact. The effect on tooling differs. With `-w` you lose types, variable locations and the line-to-instruction map, so a debugger can no longer print a typed local or set a breakpoint on a source line. With `-s` you additionally lose the symbol table, so tools that resolve a function *name* to an address have nothing to work from. ## The consequence people get wrong A stripped Go binary still prints a fully symbolised panic trace: ``` panic: runtime error: index out of range [3] with length 2 goroutine 1 [running]: main.decode(...) /src/cmd/fixtool/decode.go:41 ``` That surprises engineers coming from languages where stripping means hexadecimal addresses in every crash report. In Go the traceback comes from `pclntab`, which `-s` and `-w` do not remove. So the trade you are making is narrower than it looks: you keep crash diagnostics and lose interactive inspection. Practical rule: strip if you care about artefact size, but if there is any chance you will need to attach a debugger to that exact code or examine a core dump, keep an unstripped copy of the *same build*. A binary rebuilt later from the same commit is usually equivalent but is not guaranteed to be the artefact that produced the crash. ## A related flag that breaks stepping in a different way `go build -trimpath` removes the absolute file-system paths of the build machine from the binary, recording module-relative paths instead. It is excellent for reproducible builds and for not leaking a developer's home directory. But DWARF then names sources by a path that does not exist on your machine, so a debugger will report that it cannot find the source file even though the debug information is intact. The fix is a source-path substitution in the debugger, not a rebuild — and knowing that saves the wasted assumption that the debug data was stripped. ## Choosing per artefact A reasonable default set: - **Local debugging build**: `-gcflags="all=-N -l"`, no stripping, no `-trimpath`. Slow, large, wholly inspectable. - **Release build**: optimised, `-ldflags="-s -w"` and `-trimpath` if size and reproducibility matter, accepting that panic traces remain readable and interactive debugging does not. - **Anything you might need to inspect after the fact**: optimised, but keep the unstripped artefact somewhere retrievable. The question to ask before stripping is not "do we need debug symbols?" but "what do we lose that we cannot get from a traceback?" — because in Go the traceback survives.
- Why does a binary built with `-ldflags="-s -w"` still print function names in a panic trace?Because tracebacks do not come from DWARF. The linker embeds a program-counter line table, `pclntab`, plus function metadata that the runtime itself needs to unwind stacks, grow them and implement `recover`. `-s` and `-w` remove the debugger's metadata, not the runtime's, so crash output stays symbolised while interactive inspection is gone.
- What do you lose operationally by stripping, and how do you hedge?You lose the ability to attach a debugger to that artefact or to read typed locals out of a core dump; you are left with tracebacks and logs. The hedge is to keep the unstripped output of the exact same build somewhere retrievable, rather than assuming a later rebuild from the same commit will be byte-identical.
- Your debugger reports it cannot find the source file even though the binary was not stripped. What is the likely cause?The binary was probably built with `go build -trimpath`, which records module-relative source paths instead of the build machine's absolute paths. The DWARF is intact — the debugger simply cannot resolve those paths on your disk. Configure a source-path substitution rather than rebuilding without the flag.
saying these in an interview costs you the question
- Thinks stripping makes panic traces show raw addresses
- Believes -s and -w are the same flag spelled twice
- Assumes DWARF is what produces a panic stack trace
- Strips release binaries with no unstripped copy kept anywhere
- Blames stripping when -trimpath hid the source paths