Stop Print-Debugging Go: Conditional Breakpoints and Goroutine Inspection in GoLand
Print-debugging a Go bug that only fires on iteration 10,000 means endless edit-compile cycles. GoLand's Delve-backed debugger — conditional breakpoints, expression evaluation, and goroutine inspection — is the better habit.
15 Jan 2026, 02:32 UTC

The bug that laughs at fmt.Println
Your slice-processing function panics — but only on the 10,000th iteration, or only inside one of forty goroutines. The classic response is a print statement, which means editing code, recompiling, rerunning, sifting through ten thousand lines of output, and then remembering to delete the prints before committing. Every new hypothesis costs another full cycle.
There's a better habit: GoLand ships a debugger built on Delve, the de facto standard Go debugger, and three of its features — conditional breakpoints, expression evaluation, and goroutine inspection — replace most of that print-debugging loop. The thesis of this post is simple: making the debugger your default reaction to a confusing bug is an engineering decision that pays off within a week.
Conditional breakpoints: pause only when it matters
A plain line breakpoint on a hot loop is useless — you'd hit Continue ten thousand times. A conditional breakpoint pauses only when a Go expression you supply evaluates to true. In GoLand you set a normal breakpoint (click the gutter next to the line), then right-click it and enter a condition.
Say a function panics near the end of a loop:
func process(items []Item) int {
total := 0
for i := range items {
total += items[i+1].Weight // panics on last iteration
}
return total
}Set a breakpoint on the total += line, right-click it, and add the condition:
i == len(items)-1Run with the debugger (the bug icon next to the run configuration, or the gutter icon on a main function or test). Execution now sails through the first 9,999 iterations and stops exactly at the failing one, with i, items, and surrounding variables visible in the Debug tool window. No prints, no recompiles, no cleanup.
Two things to know about conditions:
- The expression must be a valid Go expression evaluable in the paused scope. Constructs Delve can't evaluate (some function calls, complex generics) will error or be skipped rather than pause.
- There's also an "Evaluate and log" breakpoint option that prints values to the debug console without suspending execution — the disciplined, auto-cleaned-up version of a print statement.
Evaluate Expression: try the fix before you type it
Once paused, you're not limited to staring at variables. GoLand's Evaluate Expression feature (available from the Debug tool window) lets you execute arbitrary Go code against the live, paused state. In the example above, you can test the guard before editing the source:
i+1 < len(items) // true for most iterations, false at the end
items[i].Weight // confirm the value you actually wantedThis turns debugging into a conversation: form a hypothesis, evaluate it against real state, refine. When the evaluation confirms the off-by-one, you fix the source once — instead of the print-driven loop of "add log, rerun, squint, repeat." You can also modify variable values mid-session to probe how downstream code behaves with corrected input.
Goroutine inspection: where prints actively hurt you
Concurrency bugs are where print-debugging goes from tedious to counterproductive. Writing to stdout involves synchronization, so adding prints changes timing — and a heisenbug that depends on scheduling can vanish the moment you instrument it.
When GoLand's debugger pauses, it lists the program's goroutines and their stacks, and you can switch between them to inspect each one's local variables. This is the practical tool for three classic problems:
- Deadlocks: pause the hung program and look at which goroutines are blocked on which channel operations or mutexes. The stacks usually name the culprit immediately.
- Leaked goroutines: if a long-running service accumulates goroutines, pause and count how many are parked at the same line — often a channel read nobody writes to.
- Timing-sensitive bugs: because you're observing state rather than injecting I/O, you don't perturb the schedule as much as a print would.
Bonus: debug one table-driven case, not the whole suite
A complementary GoLand feature pairs nicely with all of this: table-driven tests written with t.Run get individual gutter icons per subtest. When a test table has thirty cases and one fails, you can debug just that case — the debugger attaches to a run scoped to the single failing input, so your conditional breakpoint isn't wading through twenty-nine irrelevant executions.
The honest limitations
The debugger isn't a universal solvent. It builds with reduced optimization, which changes performance characteristics — race conditions and deadline-sensitive bugs sometimes refuse to reproduce under it. For those, the race detector (go test -race) and targeted logging remain the right tools. Conditional breakpoints on very hot paths also slow execution noticeably, since the condition is evaluated on every hit. And exact menu names, shortcuts, and breakpoint options shift between GoLand releases, so treat the descriptions here as behavior-level guidance and check the current documentation for the precise clicks.
To verify the workflow yourself: create a small project, set a breakpoint, start the debugger from the gutter, and confirm variables appear when execution pauses. Then right-click the breakpoint, add a condition like i == 9999, and confirm it triggers only at that iteration. If you're curious what's underneath, running the same session with dlv debug in a terminal shows the same engine the IDE is driving.
Make it a habit, not a last resort
The actionable part is small: learn two things this week — the run-with-debugger action and the right-click-to-add-condition gesture on breakpoints. The next time a bug resists obvious reasoning, reach for those before reaching for fmt.Println. You'll cut the edit-compile-print cycle down to a single pause, and your commits will stop accumulating stray debug scaffolding that someone has to review and remove.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.