Before this:Print debugging & logging
Using a debugger
Key takeaways
A debugger pauses a live program at a breakpoint and lets you inspect
everything — variables, the call stack, other goroutines — then advance
line by line with next / step / continue. Go’s debugger is Delve
(dlv), and every major editor front-ends it. Its edge over prints: no
guessing what to print — the whole state is there, and you can chase
surprises immediately without another edit-rerun cycle. Its limits: timing
bugs, long time-scales, and remote machines — where
logging stays king.
Prints answer the questions you thought to ask. This lesson’s tool answers the follow-ups too — while the program stands frozen mid-mistake, waiting for you to look around.
The core idea: stop time
A debugger runs your program under supervision. You mark a line as a breakpoint; when execution reaches it, the program freezes — not crashed, just paused — and the debugger hands you the controls:
- Inspect any variable in scope — not just ones you pre-chose to print.
- Walk the call stack — the same frames you learned to read in a panic, but live: click into any frame and inspect its locals.
- Advance deliberately:
next(run this line, stop at the following),step(descend into the function being called),continue(run to the next breakpoint).
The experience that sells it: you’re paused where the bad value appears, and instead of adding another print and re-running, you just… look at the other seventeen variables, and the caller’s, until the surprise is found. Each “wait, what’s that?” costs seconds, not a rebuild cycle.
Delve in five minutes
Go’s debugger is Delve. Editors (VS Code, GoLand) wrap it in a UI — breakpoints as gutter clicks — and the Go module’s debugging lesson covers that setup; here’s the raw CLI, worth seeing once so the UI never feels like magic:
go install github.com/go-delve/delve/cmd/dlv@latest
dlv debug ./cmd/scan # build + run under the debugger
dlv test ./decoder/ # debug a TEST — see below
A session against a misbehaving decode:
(dlv) break decoder.DecodeBurst # breakpoint at a function
(dlv) continue # run until it's hit
> decoder.DecodeBurst() ./decoder/decode.go:37
(dlv) print len(raw) # inspect anything in scope
12
(dlv) locals # all local variables
(dlv) next # advance one line
(dlv) stack # where am I? (live call stack)
(dlv) continue # onward to the next hit
Two power moves worth learning early. dlv test debugs a test — combined
with a failing-first regression test, it
drops you inside a reproduced bug with two commands, which is the highest
debugging standard of living there is. And conditional breakpoints solve
the loop problem — a breakpoint in a hot decode loop hit 4,800 times a second
is unusable until you attach the condition that describes your suspect:
(dlv) break decode.go:52
(dlv) condition 1 frame.Talkgroup == 4521 && !frame.CRCOK
Now the program runs at (near) full speed and stops only in the interesting case — the debugger equivalent of a well-aimed hypothesis print.
Debugger or prints? An honest decision table
| Situation | Reach for |
|---|---|
| Rich state to explore, unclear what’s relevant yet | Debugger — everything inspectable, no guessing what to print |
| Bug reproduced inside a test | Debugger (dlv test) — frozen at the crime scene |
| Timing/concurrency suspect | Prints/logs — pausing changes timing; a frozen goroutine hides the race the others were losing |
| Behavior over minutes or hours (a slow drift, a leak) | Logs — you can’t single-step an afternoon; you read its recording |
| Field machine you can’t attach to | Logs — the only witness |
| One known value to check at one known point | Either — a print may honestly be faster than launching a session |
The pattern behind the rows: a debugger examines a moment in depth; logs record a history in breadth. Deep-narrow versus shallow-wide — which is why real-time signal-processing code like GopherTrunk’s leans logs-first (a paused DSP pipeline is no longer real-time, and the interesting bugs live in the flow), while a wrong-answer bug in a pure function is Delve’s home game.
Rule of thumb: debugger for what is wrong with this state; logs for how did we get here over time. Master both; choose per bug.
Quick check: which bug is a debugger LEAST suited to?
Recap
- A debugger pauses a live program at breakpoints and exposes all state — variables, live stack, goroutines.
- Core moves:
break,continue,next,step,print,stack— that vocabulary is 90% of daily use, in any front-end. - Delve (
dlv) is Go’s debugger;dlv test+ a failing regression test drops you inside a reproduced bug. - Conditional breakpoints make hot loops debuggable — stop only when your suspect condition holds.
- Debuggers examine a moment; logs record a history — timing bugs, long time-scales, and remote failures stay logs-first.
Next up: Bisecting history