Back to blog

Saturday, August 15, 2026

nil Pointer Dereference in Go: How to Actually Debug It

nil Pointer Dereference in Go: How to Actually Debug It

nil Pointer Dereference in Go: How to Actually Debug It

The runtime panicked because some *T was nil and the code followed it: a field read, a write, or a method call. The stack trace names the line. Start there. Do not wrap the call in recover and keep going — that hides the bug. Find which pointer is nil and why it was never set.

The message is always:

panic: runtime error: invalid memory address or nil pointer dereference

That is an implicit panic from the runtime, same unwind as panic(value): deferred calls run, then the process dies unless something recovers.

The failure

package main

import "fmt"

type Cache struct {
    Hits int
}

type Service struct {
    Cache *Cache
}

func (s *Service) Hit() {
    s.Cache.Hits++ // panics if Cache is nil
}

func main() {
    var s Service // Cache is nil
    s.Hit()
    fmt.Println(s.Cache.Hits)
}
panic: runtime error: invalid memory address or nil pointer dereference
goroutine 1 [running]:
main.(*Service).Hit(...)
        /tmp/main.go:15
main.main()
        /tmp/main.go:20

The first your frame is Hit at line 15. s is not nil — s.Cache is.

Why this happens

A pointer's zero value is nil. nil is not an empty struct. Following it is not defined, so the runtime panics.

The usual sources:

  1. var p *T or a struct field never assigned. Constructors that return Service{} instead of &Service{Cache: &Cache{}}.
  2. A function that returns nil on "not found". user := find(id); fmt.Println(user.Name) without checking.
  3. Map lookup of *T. Missing key → nil. Then users["x"].Name panics.
  4. Embedded *Inner that is nil. o.Value promotes to o.Inner.Value. The outer struct is fine; the inner pointer is not.
  5. A typed nil inside an interface. var p *ConsoleLogger; var l Logger = p; if l != nil { l.Log(...) } still panics. The interface is not nil (it has a type); the concrete pointer is.

Method calls on a nil receiver are legal if the method does not dereference the receiver. func (s *Service) Hit() still runs; the panic is the .Cache access inside.

How to debug it

  1. Read the stack from the top. Skip runtime. frames. The first package.function in your module is the dereference. Open that file:line.
  2. Name the pointer. On that line, every . and * is a candidate. Print or inspect each with Delve (dlv debug, p s, p s.Cache) or a one-line fmt.Printf("%#v\n", s).
  3. Walk backward to the assignment. Who was supposed to set it? A constructor, a JSON unmarshal, a test fixture, a map? Unmarshal of missing JSON object fields leaves pointers nil.
  4. Widen the trace if the panic is in a library. GOTRACEBACK=all prints every goroutine. A panic in a worker often started from a nil you passed in at spawn time.
  5. Fix the construction, not the symptom. Initialize the field, return an error instead of a nil pointer, or check if p == nil at the API boundary that produces it.
func NewService() *Service {
    return &Service{Cache: &Cache{}}
}

func (s *Service) Hit() {
    if s == nil || s.Cache == nil {
        return // or panic with a useful message, or return error
    }
    s.Cache.Hits++
}

Guarding every field is noise. Prefer types that cannot be used half-built: a constructor that always sets Cache, tests that call that constructor, and go vet / nilness analyzers for the rest.

recover belongs at goroutine boundaries in servers so one bad request does not kill the process. It is not how you debug this. After recover, log the stack and treat it as a bug to fix.