How Do I Debug Nil Pointer Dereference in Go?
How do I debug nil pointer dereference in Go?
TL;DR
- Bottom line: A nil pointer dereference panic (
runtime error: invalid memory address or nil pointer dereference) occurs when you access a field, method, or index through a pointer that isnil. The fix is always one of: initialize the pointer, check for nil before access, or return an error instead of nil. Read the stack trace line number to find which variable is nil, then trace back to where it should have been assigned. - Key tool/command:
dlv debug main.go(Delve debugger) — set breakpoints, inspect pointer values, and step through code to find where a pointer becomes nil. - Watch out for: An interface holding a nil concrete value is not
== nil. This is the #1 subtle cause of nil panics that pass standard nil checks. - Works with: All Go versions (Go 1.0+). Delve requires Go 1.21+. Static analyzers
go vet,staticcheck, andnilawaywork with Go 1.18+. Use Go 1.25+ in production — Go 1.21–1.24 had a compiler bug that could delay nil pointer panics past subsequent error checks, hiding real nil-deref bugs; Go 1.25 makes nil dereferences panic immediately as the spec requires. [src8]
Constraints
- Never ignore the return value of functions that return
(*T, error)— the pointer may be nil whenerr != nil. [src3] - Never dereference a pointer without confirming it is non-nil, especially after type assertions, map lookups, or function returns. [src1]
- Never write to a nil map — this panics. A nil slice is safe to append to, but a nil map must be initialized with
make()before writing. [src1] - Never assume an interface is nil just because the underlying value is nil — check the concrete type separately or return a plain
nilfrom functions returning interfaces. [src6] - Race conditions can set a pointer to nil between your nil check and dereference — use
sync.Mutexorsync.RWMutexfor shared pointer variables. [src3]
Quick Reference
| # | Cause | Likelihood | Signature | Fix |
|---|---|---|---|---|
| 1 | Uninitialized pointer variable | ~30% | var p *MyStruct then p.Field |
Initialize: p := &MyStruct{} or p := new(MyStruct) [src2] |
| 2 | Function returns nil pointer (error not checked) | ~25% | result := getUser(id) then result.Name |
Check: if result == nil { return err } [src3, src5] |
| 3 | Nil map write | ~15% | var m map[string]int then m["key"] = 1 |
Initialize: m := make(map[string]int) [src1] |
| 4 | Interface with nil concrete value | ~10% | var p *MyError = nil; var err error = p; err != nil is true |
Return plain nil, not typed nil [src6] |
| 5 | Uninitialized struct pointer field | ~8% | type S struct { Inner *T }; s := S{}; s.Inner.Field |
Use constructor: NewS() that initializes all fields [src7] |
| 6 | Nil method receiver | ~5% | Calling method on nil pointer receiver | Add nil guard: if p == nil { return zero } [src4] |
| 7 | Nil function value | ~3% | var fn func(); fn() |
Check: if fn != nil { fn() } [src1] |
| 8 | Race condition sets pointer to nil | ~2% | Intermittent panic under load | Add sync.Mutex around pointer access [src3] |
| 9 | Nil channel operations | ~2% | var ch chan int; ch <- 1 (blocks) or close(ch) (panics) |
Initialize: ch := make(chan int) [src1] |
Decision Tree
START — panic: runtime error: invalid memory address or nil pointer dereference
├── Read stack trace → identify exact file:line
│ └── Which variable is being dereferenced on that line?
│
├── Is it a struct pointer variable?
│ ├── Declared with `var p *T`? → Initialize: `p = &T{}` [src2]
│ ├── Returned from a function? → Check error: `if err != nil` [src3, src5]
│ └── Is it a struct field? → Initialize in constructor [src7]
│
├── Is it a map?
│ ├── Writing to nil map? → `m = make(map[K]V)` [src1]
│ └── Reading from nil map? → Safe (returns zero value), not the cause
│
├── Is it an interface?
│ ├── Interface itself is nil? → Initialize or check before calling methods
│ └── Non-nil but holds nil value? → Return plain `nil` [src6]
│
├── Is it a function value?
│ └── `var fn func()`? → Check: `if fn != nil { fn() }` [src1]
│
├── Is it intermittent / under concurrent load?
│ └── Race condition → sync.Mutex; `go run -race` [src3]
│
└── Still unclear?
├── Add fmt.Printf before panic line [src3]
├── Use Delve: `dlv debug`, breakpoint, inspect [src4]
└── Run `go vet ./...` and `staticcheck ./...` [src4]
Step-by-Step Guide
1. Read the stack trace to find the panic location
Every nil pointer panic prints a goroutine stack trace. The first line after panic: tells you the file and line number. [src3]
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x1092a9d]
goroutine 1 [running]:
main.processUser(0x0)
/app/main.go:25 +0x1d <-- file main.go, line 25
main.main()
/app/main.go:12 +0x45
Key: addr=0x0 confirms nil dereference. (0x0) in function args shows the pointer was nil.
2. Identify which pointer is nil on that line
Look for field access (p.Field), method call (p.Method()), index operation, or dereference (*p). [src3]
func processUser(u *User) {
fmt.Println(u.Name) // u is nil here
}
Verify: Add fmt.Printf("u = %v\n", u) before the line — if it prints <nil>, you found it.
3. Trace back to where the pointer should have been assigned
Walk the call chain in the stack trace upward. Find where the nil pointer was created or returned. [src5]
func main() {
user := findUser(999) // Returns nil when not found
processUser(user) // Passes nil to processUser
}
func findUser(id int) *User {
if id > 100 {
return nil // ROOT CAUSE
}
return &User{Name: "Alice"}
}
4. Apply the appropriate fix pattern
Choose based on the cause identified. [src3, src5]
// FIX A: Add nil check at call site
user := findUser(999)
if user == nil {
log.Println("user not found")
return
}
processUser(user)
// FIX B: Return error instead of nil (preferred)
func findUser(id int) (*User, error) {
if id > 100 {
return nil, fmt.Errorf("user %d not found", id)
}
return &User{Name: "Alice"}, nil
}
Verify: go run main.go — no panic. go vet ./... — no warnings.
5. Run static analysis to find other nil dereference risks
Catch similar issues across the codebase. [src4]
# Built-in
go vet ./...
# Comprehensive linter
staticcheck ./...
# Uber's nil safety analyzer
nilaway ./...
# Race detector
go test -race ./...
6. Use Delve for complex nil pointer debugging
When the nil source is not obvious from code inspection. [src4]
# Install and start
go install github.com/go-delve/delve/cmd/dlv@latest
dlv debug ./cmd/myapp
# Set breakpoint at panic line
(dlv) break main.go:25
(dlv) continue
(dlv) print u # inspect the nil pointer
(dlv) stack # full call chain
Code Examples
Go: nil-safe method receiver pattern
// Input: A struct with methods that may be called on nil receivers
// Output: Methods that return zero values instead of panicking
type Config struct {
Host string
Port int
}
func (c *Config) GetHost() string {
if c == nil {
return "localhost"
}
return c.Host
}
func (c *Config) GetPort() int {
if c == nil {
return 8080
}
return c.Port
}
func startServer(cfg *Config) {
host := cfg.GetHost() // Safe even if cfg is nil
port := cfg.GetPort()
fmt.Printf("Listening on %s:%d\n", host, port)
}
Go: generic safe dereference helpers (Go 1.18+)
// Input: Any pointer that might be nil
// Output: The value, or a safe default
func Deref[T any](p *T) T {
if p == nil {
var zero T
return zero
}
return *p
}
func DerefOr[T any](p *T, fallback T) T {
if p == nil {
return fallback
}
return *p
}
// Usage:
var name *string
fmt.Println(Deref(name)) // "" (zero value)
fmt.Println(DerefOr(name, "unknown")) // "unknown"
Go: constructor pattern preventing nil fields
// Input: Required dependencies for a service
// Output: Fully initialized struct or error
type UserService struct {
db *sql.DB
cache *redis.Client
logger *slog.Logger
}
func NewUserService(db *sql.DB, cache *redis.Client, logger *slog.Logger) (*UserService, error) {
if db == nil {
return nil, fmt.Errorf("NewUserService: db must not be nil")
}
if cache == nil {
return nil, fmt.Errorf("NewUserService: cache must not be nil")
}
if logger == nil {
logger = slog.Default()
}
return &UserService{db: db, cache: cache, logger: logger}, nil
}
Anti-Patterns
Wrong: Ignoring error return and dereferencing pointer
// BAD — if GetUser returns nil on error, this panics [src3]
user, _ := svc.GetUser(ctx, id)
fmt.Println(user.Name) // panic if user is nil
Correct: Always check error before using pointer
// GOOD — handle error first [src3]
user, err := svc.GetUser(ctx, id)
if err != nil {
return fmt.Errorf("get user %d: %w", id, err)
}
fmt.Println(user.Name) // safe
Wrong: Returning typed nil pointer in interface return
// BAD — caller's err != nil check will be TRUE even on success [src6]
func validate(s string) error {
var err *ValidationError // typed nil
if s == "" {
err = &ValidationError{Msg: "empty"}
}
return err // non-nil interface with nil *ValidationError!
}
// validate("hello") returns non-nil error!
Correct: Return plain nil for interface types
// GOOD — return untyped nil explicitly [src6]
func validate(s string) error {
if s == "" {
return &ValidationError{Msg: "empty"}
}
return nil // plain nil — err == nil works correctly
}
Wrong: Writing to uninitialized map
// BAD — nil map write panics [src1]
var counts map[string]int
counts["hello"] = 1 // panic: assignment to entry in nil map
Correct: Initialize map before use
// GOOD — always use make() for maps [src1]
counts := make(map[string]int)
counts["hello"] = 1 // safe
Wrong: No nil guard on method receiver
// BAD — panics when Next is nil [src4]
func (n *Node) String() string {
return fmt.Sprintf("%d -> %s", n.Value, n.Next.String())
}
Correct: Nil guard on receiver
// GOOD — handle nil receiver explicitly [src4]
func (n *Node) String() string {
if n == nil {
return "<nil>"
}
return fmt.Sprintf("%d -> %s", n.Value, n.Next.String())
}
Common Pitfalls
- Interface nil confusion: An interface holding a nil pointer is NOT
== nil. Always return plainnilfrom functions returning interfaces, not a typed nil variable. [src6] - Unchecked error returns: Functions returning
(*T, error)can return nil for the pointer when err is non-nil. Always check error before using the pointer. [src3] - Nil map vs nil slice asymmetry: A nil slice is safe for
len(),range, andappend(). A nil map panics on write. Alwaysmake()maps before writing. [src1] - Struct zero value has nil pointer fields:
MyStruct{}initializes value fields to zero but pointer fields to nil. Use constructor functions. [src7] - Race conditions with shared pointers: A pointer checked for nil in one goroutine can be set to nil by another between check and dereference. Use
sync.Mutex; rungo test -race. [src3] - Type assertion on nil interface panics:
x.(ConcreteType)panics ifxis nil. Use the comma-ok form:v, ok := x.(ConcreteType). [src1]
Diagnostic Commands
# Run with race detector
go run -race main.go
go test -race ./...
# Static analysis
go vet ./...
staticcheck ./...
nilaway ./...
# Delve debugger
go install github.com/go-delve/delve/cmd/dlv@latest
dlv debug ./cmd/myapp
# break main.go:25 → continue → print varName → stack
# Build with full debug info
go build -gcflags="all=-N -l" -o myapp ./cmd/myapp
# Print all goroutine stacks (Linux/macOS)
kill -SIGQUIT <pid>
# Verbose test output
go test -v -count=1 ./...
Version History & Compatibility
| Feature | Available Since | Notes |
|---|---|---|
| Nil pointer panic behavior | Go 1.0 | Fundamental language spec — stable [src1] |
go vet nil checks |
Go 1.1 | Basic nil dereference detection [src1] |
Race detector (-race) |
Go 1.1 | Detects concurrent nil pointer races [src1] |
| Delve debugger | Go 1.5+ | Full nil pointer inspection; latest requires Go 1.21+ [src4] |
Generic helpers (Deref[T]) |
Go 1.18 | Requires generics support [src4] |
staticcheck nil analysis |
Go 1.18+ | SA5011 checks for nil pointer dereference [src4] |
nilaway (Uber) |
Go 1.20+ | Cross-function nil flow analysis [src4] |
| GoLand interprocedural nil analysis | GoLand 2025.2 (Jul 2025) | Tracks nil flow across files/packages with a Data Flow Analysis tool window; <5% build overhead [src9] |
| Nil pointer panic correctness fix | Go 1.25 (Aug 2025) | Reverses a Go 1.21–1.24 compiler bug that delayed nil checks past subsequent statements; nil deref now panics immediately as spec requires [src8] |
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Panic says "invalid memory address or nil pointer dereference" | Panic says "index out of range" | Bounds checking / len() guard |
| Stack trace points to pointer field access | Error is "assignment to entry in nil map" | make(map[K]V) initialization |
| Intermittent crash under concurrent load | Deterministic crash on startup | Check initialization order |
Interface != nil but method panics |
Interface is truly nil | Standard nil check suffices |
Important Caveats
- Recover does not fix nil dereference: While
defer/recovercan catch nil pointer panics, it masks the bug. The program state is likely corrupt. Only use recover at top-level request handlers as a safety net. [src1] - Nil pointer panics are deterministic (unless concurrent): If intermittent, suspect a race condition. Run
go test -race. Non-concurrent nil panics always reproduce with the same inputs. [src3] - Different nil types are not equal in interfaces:
(*int)(nil)and(*string)(nil)assigned tointerface{}produce different non-nil values. [src6] new()returns a pointer to zeroed memory, not nil:p := new(MyStruct)gives&MyStruct{}, not nil. But pointer fields within that struct are still nil. [src2]- Go 1.21–1.24 could silently skip nil panics: A compiler bug introduced in Go 1.21 sometimes reordered nil checks so a dereference like
f.Name()ran before the matchingif err != nil { return }— masking the bug instead of panicking. Go 1.25 (Aug 2025) restores the spec-mandated immediate panic. If you maintain a service still on Go 1.21–1.24, assume any "impossible" panic-free code path through(*T, error)returns is a hidden nil bug and audit it. [src8]