Debugging with Delve
Delve (dlv) is the standard debugger for Go.
It stops a running program at breakpoints, steps through code, inspects goroutines, and reads variable values using DWARF debug information from the compiler.
Summary
Use Delve when tests and logs are not enough to explain incorrect state, especially with concurrency, interfaces, or third-party callbacks.
The CLI and IDE integrations share the same backend: breakpoints, continue, next, step, and goroutine listing.
Remote and headless modes attach to processes in containers or Kubernetes pods when you can reproduce issues outside your laptop.
Recipe
Quick-reference recipe card - copy-paste ready.
# Debug current package main
dlv debug .
# Debug a test function
dlv test . -- -test.run TestName
# Common REPL commands after break
break main.main
continue
next
step
goroutines
print errWhen to reach for this:
- A panic occurs only under load with many goroutines.
- An interface holds an unexpected concrete type.
- You need to inspect loop variables at a specific iteration.
- Production-like repro runs in Docker and you can attach locally.
Working Example
// example.com/demo/internal/counter/counter.go
package counter
import "sync"
type Counter struct {
mu sync.Mutex
n int
}
func (c *Counter) Inc() {
c.mu.Lock()
c.n++
c.mu.Unlock()
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.n
}// example.com/demo/internal/counter/counter_test.go
package counter
import (
"sync"
"testing"
)
func TestConcurrentInc(t *testing.T) {
var c Counter
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
c.Inc()
}()
}
wg.Wait()
if c.Value() != 10 {
t.Fatalf("got %d", c.Value())
}
}Debug the failing test:
cd internal/counter
dlv test . -- -test.run TestConcurrentIncIn the Delve shell:
(dlv) break counter_test.go:22
(dlv) continue
(dlv) goroutines
(dlv) goroutine 5 bt
(dlv) print c.n
What this demonstrates:
dlv testbuilds test binaries with debug symbols and passes flags after--togo test.- Breakpoints can target test source lines.
goroutineslists concurrent stacks; switch withgoroutine <id>.printshows live memory, including struct fields behind mutexes (mind lock ordering).
Deep Dive
How It Works
- Go compiler emits DWARF debug info when building with default debug settings.
- Delve controls the inferior process with OS debug APIs (
ptraceon Linux, equivalents elsewhere). - Goroutine-aware stack walks use runtime metadata, not only thread-local stacks.
- The Delve RPC API (v2) powers IDE clients.
Core Commands
| Command | Purpose |
|---|---|
break / clear | Set or remove breakpoints by location or function |
continue / next / step | Run, step over, step into |
restart | Restart process from entry (debug session) |
goroutines | List goroutines with IDs and states |
stack / bt | Backtrace for selected goroutine |
print / examine | Evaluate expressions in current frame |
cond | Break only when expression is true |
Testing and Non-Main Packages
dlv test ./... -- -test.run TestLeak -count=1Use -count=1 to disable test cache while iterating.
For main packages in cmd/:
dlv debug ./cmd/apiRemote and Headless Debugging
dlv debug --headless --listen=:2345 --api-version=2 --accept-multiclient .IDE launch configs attach to 127.0.0.1:2345.
In Kubernetes, port-forward the pod debug port; restrict network access because Delve grants memory inspection.
Go Notes
# When variables are optimized away
go build -gcflags="all=-N -l" -o bin/app ./cmd/app
dlv exec ./bin/app-N disables optimizations; -l disables inlining.
Use only for debug builds, not production release artifacts.
Gotchas
- Optimized-out variables - Inlined functions hide locals in
print. Fix: Rebuild with-gcflags="all=-N -l"for the debug session. - Debugging stripped binaries -
-ldflags="-s -w"removes debug info. Fix: Keep symbols in debug/staging images; strip only release builds you never attach to. - Breakpoint on wrong goroutine - A break triggers on any goroutine hitting the line. Fix: Use
condwith goroutine-specific state or break closer to the suspect branch. - Time-based races disappear - Stopping threads changes timing. Fix: Combine Delve with
-racebuilds for data races; use Delve for state inspection after repro. - Delve in Alpine without compat - musl vs glibc mismatches break attach. Fix: Use debug images based on distro matching build, or
dlvbuilt for target libc. - Production attach risk - Pausing a live service drops traffic. Fix: Reproduce in staging, use core dumps, or ephemeral debug pods with auth.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
fmt.Printf / structured logs | Simple flow tracing | Values are too large or concurrency order matters |
runtime/trace and pprof | Throughput, latency, blocking profiles | You need exact local variable values at one line |
testing + testify | Regression prevention | Bug needs interactive exploration |
Core dump analysis (dlv core) | Post-mortem on crashed process | Live iteration is faster locally |
FAQs
How is Delve different from GDB for Go?
Delve understands goroutines, Go strings, interfaces, and channel layouts.
GDB treats Go programs more like C and is painful for routine Go debugging.
Can I debug a running Docker container?
Yes - copy dlv into the container or use a debug sidecar, run headless, and port-forward.
Ensure the binary inside retains DWARF symbols.
Does Delve work with modules and vendoring?
Yes - it debugs the built binary.
Source paths should match the build tree so breakpoints resolve to files.
How do I break on panic?
Set break runtime.fatalpanic or run with GOTRACEBACK=crash and analyze the stack at the break.
Many teams break on panic call sites in tests instead.
Can Delve evaluate calls in the debugged process?
Limited - some expressions with function calls work; side effects can mutate live state.
Prefer print of variables over arbitrary calls.
Why does `dlv test` rebuild every time?
Test binaries change as you edit.
Use -test.count=1 and keep the session open to amortize rebuild cost.
How do I watch a variable?
Use conditional breakpoints with cond on expressions like n > 100.
Full watchpoints depend on platform support; loops often use conditional breaks.
Is remote debugging over TLS supported?
Delve RPC is typically local or tunneled via SSH/port-forward.
Do not expose the raw port to the public internet.
Can I debug cgo code?
Partially - Delve steps through Go frames reliably; C frames may need platform debuggers.
Keep cgo boundaries thin for easier inspection.
Should Delve replace the race detector?
No - they solve different problems.
Use -race in CI; use Delve to inspect state once a race is suspected or reproduced.
Related
- Go Developer Tooling: LSP, Debugger, and Analyzers - where Delve fits in the toolchain
- Go Tooling Setup Basics - install
dlvand first commands - gopls: Navigation, Refactoring & Diagnostics - static navigation before runtime debug
- Go Debugging Basics - broader defect workflow patterns
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).