Skip to main content
โšก Calmops

Go Nil Pointer Panics: Causes, Prevention, and Debugging

Introduction

runtime error: invalid memory address or nil pointer dereference is one of the most common panics in Go. It happens when you try to access a method or field on a nil pointer. This guide explains why it happens, the subtle nil interface trap, and how to write nil-safe code.

Why Nil Pointer Panics Happen

In Go, nil is the zero value for pointers, interfaces, maps, slices, channels, and functions. Dereferencing a nil pointer โ€” accessing its fields or calling its methods โ€” causes a runtime panic:

type User struct {
    Name string
}

func (u *User) Greet() string {
    return "Hello, " + u.Name  // panics if u is nil
}

var u *User  // u is nil
u.Greet()   // panic: runtime error: invalid memory address or nil pointer dereference

Common Causes

1. Unchecked Error Returns

The most common cause: ignoring the error and using the nil result:

// getDoc returns (nil, error) when document not found
func getDoc(url string) (*Document, error) {
    if url == "" {
        return nil, fmt.Errorf("empty URL")
    }
    return &Document{Title: "Found"}, nil
}

func main() {
    doc, _ := getDoc("")  // ignoring the error!
    doc.Process()         // panic: doc is nil
}

Fix: Always check errors before using the result:

doc, err := getDoc(url)
if err != nil {
    log.Printf("getDoc failed: %v", err)
    return
}
doc.Process()  // safe

2. Uninitialized Struct Fields

type Config struct {
    Logger *log.Logger  // pointer field, zero value is nil
}

cfg := Config{}
cfg.Logger.Println("hello")  // panic: Logger is nil

Fix: Initialize pointer fields:

cfg := Config{
    Logger: log.New(os.Stdout, "", log.LstdFlags),
}

3. Map Access Returning Nil

type Handler func(string) string

handlers := map[string]Handler{
    "greet": func(s string) string { return "Hello, " + s },
}

h := handlers["unknown"]  // h is nil (zero value for func type)
h("world")                // panic: nil function call

Fix: Check map existence:

h, ok := handlers["unknown"]
if !ok {
    log.Println("handler not found")
    return
}
h("world")

4. Interface Nil Trap

This is the most subtle nil issue in Go. An interface value is nil only when both its type and value are nil:

type Animal interface {
    Sound() string
}

type Dog struct{}
func (d *Dog) Sound() string { return "Woof" }

func getAnimal(wantDog bool) Animal {
    var d *Dog  // d is nil (typed nil pointer)
    if wantDog {
        return d  // returns non-nil interface! (type=*Dog, value=nil)
    }
    return nil  // returns nil interface (type=nil, value=nil)
}

a := getAnimal(true)
fmt.Println(a == nil)  // false! interface is not nil
a.Sound()              // panic: nil pointer dereference inside Sound()

Why: The interface a has type *Dog and value nil. The interface itself is not nil โ€” it has a type. But calling Sound() dereferences the nil *Dog pointer.

Fix: Return nil directly, not a typed nil:

func getAnimal(wantDog bool) Animal {
    if wantDog {
        d := &Dog{}  // non-nil pointer
        return d
    }
    return nil  // true nil interface
}

Or check for nil before returning:

func getAnimal(wantDog bool) Animal {
    var d *Dog
    if wantDog {
        d = &Dog{}
    }
    if d == nil {
        return nil  // return nil interface, not typed nil
    }
    return d
}

Nil-Safe Method Design

You can write methods that handle nil receivers gracefully:

type Node struct {
    Value int
    Next  *Node
}

// Nil-safe method
func (n *Node) String() string {
    if n == nil {
        return "<nil>"
    }
    return fmt.Sprintf("%d -> %s", n.Value, n.Next.String())
}

// Works even with nil
var head *Node
fmt.Println(head.String())  // => "<nil>" (no panic)

head = &Node{Value: 1, Next: &Node{Value: 2}}
fmt.Println(head.String())  // => "1 -> 2 -> <nil>"

Defensive Nil Checks

// Check before accessing
func processUser(u *User) error {
    if u == nil {
        return errors.New("user is nil")
    }
    // safe to use u
    return nil
}

// Use pointer-to-pointer for optional values
type Config struct {
    Timeout *time.Duration  // nil means "use default"
}

func getTimeout(cfg *Config) time.Duration {
    if cfg == nil || cfg.Timeout == nil {
        return 30 * time.Second  // default
    }
    return *cfg.Timeout
}

Debugging Nil Panics in Production

Stack Traces

When a panic occurs, Go prints a stack trace. Read it from bottom to top:

goroutine 1 [running]:
main.(*Document).Process(...)          โ† the nil dereference
    /app/main.go:15
main.handleRequest(...)                โ† called from here
    /app/handler.go:42
net/http.(*ServeMux).ServeHTTP(...)
    ...

Recover with Stack Trace

func safeHandler(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                // Log the full stack trace
                buf := make([]byte, 4096)
                n := runtime.Stack(buf, false)
                log.Printf("PANIC: %v\n%s", err, buf[:n])
                http.Error(w, "Internal Server Error", 500)
            }
        }()
        h(w, r)
    }
}

Using Delve Debugger

# Install delve
go install github.com/go-delve/delve/cmd/dlv@latest

# Debug a panic
dlv debug ./cmd/server

# Set breakpoint before the panic
(dlv) break main.go:42
(dlv) continue
(dlv) print doc  # inspect the nil value

Preventing Nil Panics: Checklist

// 1. Always check errors
result, err := operation()
if err != nil { return err }

// 2. Initialize struct pointer fields
type Server struct {
    logger *slog.Logger
}
func NewServer() *Server {
    return &Server{
        logger: slog.Default(),  // initialized
    }
}

// 3. Use the comma-ok idiom for maps
val, ok := myMap[key]
if !ok { /* handle missing */ }

// 4. Never return typed nil from interface-returning functions
func getWriter() io.Writer {
    var buf *bytes.Buffer  // typed nil
    return buf             // BAD: non-nil interface wrapping nil pointer
    // return nil          // GOOD: true nil interface
}

// 5. Use nil-safe methods for recursive/linked structures
func (n *Node) Len() int {
    if n == nil { return 0 }
    return 1 + n.Next.Len()
}

The go vet and staticcheck Tools

# go vet catches some nil issues
go vet ./...

# staticcheck catches more
go install honnef.co/go/tools/cmd/staticcheck@latest
staticcheck ./...

# nilaway: dedicated nil analysis (experimental)
go install go.uber.org/nilaway/cmd/nilaway@latest
nilaway ./...

Resources

Comments