Go Debugging Basics
10 examples to get you started with Defect Scenarios - 7 basic and 3 intermediate.
Prerequisites
- Install Go 1.26.x or later from go.dev/dl.
- Install Delve:
go install github.com/go-delve/delve/cmd/dlv@latest. - Create a module:
mkdir debuglab && cd debuglab && go mod init example.com/debuglab. - Save snippets as
main.goor test files and run withgo run .orgo test -v.
Basic Examples
1. Printf with useful verbs
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))
}%#vprints Go syntax representation, helpful for nil vs empty slice.- Always log
lenandcapwhen debugging append and sub-slice bugs. - Prefer
log.Printfin servers so timestamps and file lines appear.
Related: Slice Header Copy & Append Surprise - when len/cap lie
2. Log fatal errors with stack context
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)
}
}%wpreserves the error chain forerrors.Isanderrors.As.log.Fatalfexits with code 1 after printing.- In libraries, return errors instead of logging.
Related: Debugging Go: Observable Failures and Sharp Edges - symptom families
3. Read a panic stack trace
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
}- The top frames show where the panic occurred; walk down for the caller chain.
- Look for interface conversions and returned nil pointers above the panic line.
- Run with
GOTRACEBACK=allfor full stacks including runtime frames.
Related: Nil Interface vs Nil Pointer Defect Walkthrough - typed nil traps
4. Delve: break and continue
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.breaksets a breakpoint;continueruns until it hits.nextsteps over lines;stepsteps into calls.
Related: Debugging with Delve - full Delve reference
5. Delve: inspect goroutines
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
goroutinesgoroutineslists each goroutine with stack snippet.- Compare goroutine IDs when one path blocks forever.
- Use
goroutine <id> btfor a full backtrace of one goroutine.
Related: Goroutine Leaks & Missing Context Cancellation - blocked stacks
6. Run tests under the race detector
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 ./...-raceinstruments memory access and reports concurrent read/write.- Race output includes goroutine stacks for both sides of the race.
- Run
-racein CI for packages that share mutable state.
Related: Data Race Scenarios & race Detector Fixes - fix patterns
7. Dump goroutine profile locally
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- Import
_ "net/http/pprof"registers handlers onDefaultServeMux. - Goroutine profile shows where each goroutine is blocked.
- Use the same endpoint against staging when reproducing leaks.
Related: Goroutine Leaks & Missing Context Cancellation - interpret profiles
Intermediate Examples
8. Debug a test with Delve
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 testwrapsgo testwith debugger support.- Pass test flags after
--. - Set breakpoints in test helpers the same way as production code.
Related: Debugging with Delve -
dlv testworkflows
9. Log HTTP request context on errors
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))
}- Thread IDs through
context.Contextinstead of global variables. - Log
request_idon every error path to correlate with traces. - Pass
r.Context()into downstream I/O so cancellation shows up in profiles.
Related: Goroutine Leaks & Missing Context Cancellation - ctx in handlers
10. Compare monotonic vs wall clock in logs
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.Parsewithout zone in the layout uses UTC for offset-less strings.Equalcompares instants;Formatoutput depends onLocation().- Log
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).