Debugging with Delve
Delve (dlv) is the standard debugger for Go.
Search across all documentation pages
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.
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.
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:
// 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 test builds test binaries with debug symbols and passes flags after -- to go test.goroutines lists concurrent stacks; switch with goroutine <id>.print shows live memory, including struct fields behind mutexes (mind lock ordering).ptrace on Linux, equivalents elsewhere).| 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 |
dlv test ./... -- -test.run TestLeak -count=1Use -count=1 to disable test cache while iterating.
For main packages in cmd/:
dlv debug ./cmd/apidlv 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.
# 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.
print. Fix: Rebuild with -gcflags="all=-N -l" for the debug session.-ldflags="-s -w" removes debug info. Fix: Keep symbols in debug/staging images; strip only release builds you never attach to.cond with goroutine-specific state or break closer to the suspect branch.-race builds for data races; use Delve for state inspection after repro.dlv built for target libc.| 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 |
Delve understands goroutines, Go strings, interfaces, and channel layouts.
GDB treats Go programs more like C and is painful for routine Go debugging.
Yes - copy dlv into the container or use a debug sidecar, run headless, and port-forward.
Ensure the binary inside retains DWARF symbols.
Yes - it debugs the built binary.
Source paths should match the build tree so breakpoints resolve to files.
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.
Limited - some expressions with function calls work; side effects can mutate live state.
Prefer print of variables over arbitrary calls.
Test binaries change as you edit.
Use -test.count=1 and keep the session open to amortize rebuild cost.
Use conditional breakpoints with cond on expressions like n > 100.
Full watchpoints depend on platform support; loops often use conditional breaks.
Delve RPC is typically local or tunneled via SSH/port-forward.
Do not expose the raw port to the public internet.
Partially - Delve steps through Go frames reliably; C frames may need platform debuggers.
Keep cgo boundaries thin for easier inspection.
No - they solve different problems.
Use -race in CI; use Delve to inspect state once a race is suspected or reproduced.
dlv and first commandsStack 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).
Reviewed by Chris St. John·Last updated Jul 16, 2026