Skip to main content

Go Nil Pointer Panics: Causes, Prevention, and Debugging

Created: March 12, 2022 5 min read

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. See Go Installation Guide, Go Ecosystem Overview, Go Best Practices for more context.

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

Share this article

Scan to read on mobile