Go Debugging Basics
10 examples to get you started with Defect Scenarios - 7 basic and 3 intermediate.
Search across all documentation pages
10 examples to get you started with Defect Scenarios - 7 basic and 3 intermediate.
go install github.com/go-delve/delve/cmd/dlv@latest.mkdir debuglab && cd debuglab && go mod init example.com/debuglab.main.go or test files and run with go run . or go test -v.Log values with formatting that exposes type and structure.
package main
import "fmt"
func main() {
s := []int{1, 2, 3}
fmt.Printf("s=%#v len=%d cap=%d\n", s, len(s), cap(s))
}%#v prints Go syntax representation, helpful for nil vs empty slice.len and cap when debugging append and sub-slice bugs.log.Printf in servers so timestamps and file lines appear.Related: Slice Header Copy & Append Surprise - when len/cap lie
Wrap errors and print before exit.
package main
import (
"fmt"
"log"
"os"
)
func loadConfig(path string) error {
_, err := os.Open(path)
if err != nil {
return fmt.Errorf("loadConfig(%q): %w", path, err)
}
return nil
}
func main() {
if err := loadConfig("missing.yaml"); err != nil {
log.Fatalf("startup failed: %v", err)
}
}%w preserves the error chain for errors.Is and errors.As.log.Fatalf exits with code 1 after printing.Related: Debugging Go: Observable Failures and Sharp Edges - symptom families
Trigger a nil pointer to study stack output.
package main
type Store struct{}
func (s *Store) Get() int { return 1 }
func main() {
var s *Store
_ = s.Get() // panic: nil pointer
}GOTRACEBACK=all for full stacks including runtime frames.Related: Nil Interface vs Nil Pointer Defect Walkthrough - typed nil traps
Start a debugger session on a simple program.
package main
import "fmt"
func sum(nums []int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Println(sum([]int{1, 2, 3}))
}dlv debug .
# at (dlv) prompt:
break main.sum
continue
print total
nextdlv debug . builds and starts under the debugger.break sets a breakpoint; continue runs until it hits.next steps over lines; step steps into calls.Related: Debugging with Delve - full Delve reference
List goroutines when debugging concurrency.
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("work")
}()
}
wg.Wait()
}dlv debug .
break main.main
continue
goroutinesgoroutines lists each goroutine with stack snippet.goroutine <id> bt for a full backtrace of one goroutine.Related: Goroutine Leaks & Missing Context Cancellation - blocked stacks
Catch unsynchronized access during tests.
package counter
import "testing"
var n int
func Inc() { n++ }
func TestInc(t *testing.T) {
done := make(chan struct{})
go func() { Inc(); close(done) }()
Inc()
<-done
}go test -race ./...-race instruments memory access and reports concurrent read/write.-race in CI for packages that share mutable state.Related: Data Race Scenarios & race Detector Fixes - fix patterns
Expose pprof for leak hunts.
package main
import (
"log"
"net/http"
_ "net/http/pprof"
)
func main() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}go run .
# another terminal:
curl -s localhost:6060/debug/pprof/goroutine?debug=1 | head_ "net/http/pprof" registers handlers on DefaultServeMux.Related: Goroutine Leaks & Missing Context Cancellation - interpret profiles
Stop inside a failing test function.
package mathutil
import "testing"
func Add(a, b int) int { return a + b }
func TestAdd(t *testing.T) {
got := Add(2, 2)
if got != 4 {
t.Fatalf("got %d want 4", got)
}
}dlv test . -- -test.run TestAdd
break mathutil.TestAdd
continue
print gotdlv test wraps go test with debugger support.--.Related: Debugging with Delve -
dlv testworkflows
Tie logs to request IDs during handler debugging.
package main
import (
"context"
"log"
"net/http"
)
type ctxKey string
const requestIDKey ctxKey = "requestID"
func withID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, requestIDKey, id)
}
func idFrom(ctx context.Context) string {
v, _ := ctx.Value(requestIDKey).(string)
return v
}
func handler(w http.ResponseWriter, r *http.Request) {
ctx := withID(r.Context(), "req-42")
if err := work(ctx); err != nil {
log.Printf("request_id=%s err=%v", idFrom(ctx), err)
http.Error(w, "error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func work(ctx context.Context) error { return nil }
func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe(":8080", nil))
}context.Context instead of global variables.request_id on every error path to correlate with traces.r.Context() into downstream I/O so cancellation shows up in profiles.Related: Goroutine Leaks & Missing Context Cancellation - ctx in handlers
Debug time-related defects with explicit locations.
package main
import (
"fmt"
"time"
)
func main() {
utc := time.Date(2026, 7, 15, 12, 0, 0, 0, time.UTC)
local, err := time.Parse(time.RFC3339, "2026-07-15T12:00:00-07:00")
if err != nil {
panic(err)
}
fmt.Println("utc", utc.Format(time.RFC3339))
fmt.Println("local", local.Format(time.RFC3339))
fmt.Println("equal?", utc.Equal(local))
}time.Parse without zone in the layout uses UTC for offset-less strings.Equal compares instants; Format output depends on Location().t.Location() and RFC3339 when debugging timestamp bugs.Related: Time Zone & time.Parse Gotchas - JSON and DST edges
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).
Reviewed by Chris St. John·Last updated Jul 18, 2026