Debugging Go Tests Faster with GoLand's Delve Integration
GoLand's bundled Delve debugger gives you conditional breakpoints, inline variable values, and one-click test debugging that works across multi-module workspaces. Here's how to use them effectively and where they fall short.
22 Sept 2025, 02:27 UTC

The Problem: Stopping at the Right Moment
You're debugging a flaky test that fails once every fifty runs. The failure happens deep in a loop processing thousands of records, but you only care about the iteration where record.ID == "corrupt-42". Adding fmt.Println spam and re-running is slow. Conditional breakpoints solve this, but only if they evaluate in the right context without deadlocking the debugger.
GoLand bundles Delve and surfaces its conditional breakpoints, inline variable values, and test-specific launch configurations directly in the editor. This post walks through the workflows that actually save time, where they fall short, and how to verify they work in your project.
Conditional Breakpoints That Evaluate in Target Context
Right-click the gutter next to any line and choose More → Condition. You get three modes:
- Expression: any valid Go expression that returns
bool—len(slice) > 10,user.Role == "admin",strings.Contains(msg, "timeout"). - Hit count: stop only on the Nth hit, or every N hits after a threshold.
- Log message: print a formatted string to the debug console without stopping (useful for printf-style tracing without recompiling).
The expression runs in the stopped goroutine's stack frame, so it sees all local variables, package-level symbols, and even unexported fields. It cannot call functions that need a running runtime (e.g., fmt.Println, time.Sleep) and will deadlock if the condition blocks on a channel owned by the same goroutine.
Inline Variable Values and the Hover Inspector
Enable Settings → Build, Execution, Deployment → Debugger → Go → Show inline values. During a debug session, each line shows the current value of variables in scope right in the editor gutter. Hover any variable to open a structured inspector that:
- Dereferences pointers automatically
- Expands slices, maps, and structs recursively
- Lets you evaluate arbitrary expressions in a side panel without stopping
Values refresh only on breakpoint hit or explicit Evaluate action — they don't update live while the program runs. In tight loops, the displayed value may be stale by the time you look.
Worked Example: Debugging a Single Test in a Multi-Module Workspace
Assume a Go 1.22+ workspace with two modules:
myworkspace/
go.work
consumer/
go.mod // module github.com/example/consumer
consumer_test.go
lib/
go.mod // module github.com/example/lib
processor.go
go.work contains:
go 1.22
use (
./consumer
./lib
)
Goal: Set a breakpoint in lib/processor.go and hit it when running TestProcessBatch from consumer/consumer_test.go.
- Open the workspace in GoLand 2024.2+ (File → Open → select
myworkspace). - In
lib/processor.go, set a conditional breakpoint on the line that processes each record:record.ID == "corrupt-42". - In
consumer/consumer_test.go, click the green gutter icon next tofunc TestProcessBatchand choose Debug 'TestProcessBatch'. - GoLand launches Delve with
-test.run=^TestProcessBatch$, respecting build tags andgo:generateoutput. The test runner UI shows per-test pass/fail with inline stack traces. - When the condition matches, the breakpoint hits. Hover
recordin the editor to expand its fields; the inline inspector shows the full struct without extra clicks.
This works because GoLand indexes each module's build constraints and maps breakpoints across module boundaries in the same go.work.
Trade-offs and Limitations
| Feature | Limitation | Mitigation |
|---|---|---|
| Conditional breakpoints | Cannot call runtime-dependent functions; deadlock risk on channel ops | Keep conditions pure: comparisons, len(), field access. Avoid function calls. |
| Inline values | Stale in tight loops; refresh only on stop | Use "Evaluate" action (Alt+F8) for live snapshot; add a temporary breakpoint one iteration later. |
| Multi-module workspaces | Initial breakpoint resolution slows with many modules | Exclude vendor/ and generated dirs in Settings → Go Modules → Excluded directories. |
| Remote debugging (SSH/Docker/WSL2) | Requires matching Delve version on target; Go 1.22+ iterators need compatible dlv dap | Pin Delve version in container image; use GoLand's SSH config to launch dlv dap --listen=:2345 --headless=true --api-version=2. |
| Cgo debugging | GoLand shows only Go frames; C frames need external gdb/lldb | Attach native debugger separately; set CGO_ENABLED=1 and ensure C toolchain present. |
Verification Checklist
Before relying on these features in a critical session, run through these quick checks in your environment:
- Open a Go 1.22+ module in GoLand 2024.2+, set a conditional breakpoint with
len(slice) > 10, run a test in debug mode, confirm it hits only when true. - Hover a variable during a paused test; expand a map or pointer to verify dereferencing works.
- Create a
go.workwith two local modules, set a breakpoint in the shared library, run a test from the consumer, verify the breakpoint triggers. - Toggle Show inline values off and on; observe the gutter difference.
- For remote: use GoLand's SSH config to launch a Docker container with
dlv dap --listen=:2345 --headless=true --api-version=2, attach, confirm variables and breakpoints behave identically to local.
Closing: Start with the Test Runner
The highest-leverage habit: always launch tests from the gutter icon instead of go test -v ./... in the terminal. You get conditional breakpoints, inline values, per-test pass/fail, and stack traces without changing your workflow. If you hit a limitation (stale inline values, deadlock in condition), you now know the exact cause and the workaround. The debugger is part of the editor — use it where you write code.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.