Skip to content

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

FixDevs · (Updated: )

Part of:  Go, Rust & Systems Errors

Quick Answer

How to fix Go nil pointer dereference panics — checking for nil before use, nil interface traps, nil map writes, receiver methods on nil, and defensive nil handling patterns.

The Error

Your Go program panics with:

goroutine 1 [running]:
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x...]

goroutine 1 [running]:
main.getUsername(...)
        /app/main.go:23 +0x18
main.main()
        /app/main.go:15 +0x28
exit status 2

Or you see it in an HTTP server that suddenly crashes:

2026/03/19 10:23:45 http: panic serving 127.0.0.1:51234:
runtime error: invalid memory address or nil pointer dereference

The stack trace points to a line where you’re accessing a field or calling a method on a pointer that is nil.

Why This Happens

In Go, a pointer, interface, map, slice, channel, or function value that hasn’t been initialized is nil. Accessing a field or calling a method on nil causes a panic at runtime. Unlike a compile-time error, this only surfaces when the code path that triggers the nil access actually executes, which is why it often appears in production under edge cases that unit tests didn’t cover.

The root issue is that Go’s type system doesn’t distinguish between “pointer that might be nil” and “pointer that is guaranteed non-nil.” Every pointer type *T implicitly includes nil as a valid value, and the compiler won’t warn you if you skip a nil check. This is a deliberate design choice — Go favors simplicity over the compile-time safety that languages like Rust or Kotlin provide — but it shifts the burden to the developer to guard every dereference.

Common triggers include: uninitialized pointer (var p *MyStruct declares a nil pointer, and accessing p.Field panics), function returning nil on error path (a function returns (*MyStruct, error) and you ignore the error, then dereference the nil pointer), nil interface (an interface variable containing a nil concrete value behaves differently from a true nil interface), nil map write (var m map[string]int; m["key"] = 1 panics because maps must be initialized with make), nil receiver (calling a method on a nil pointer receiver panics unless the method explicitly handles nil), and chained nil access (user.Address.City panics if user.Address is nil, even if user is not nil).

How Other Languages Handle Null

Go’s nil pointer dereference is a runtime panic, but the same class of bug exists in every language that allows null references. The difference is how each language’s type system and runtime treat it.

Rust eliminates null pointers entirely. There is no null in Rust. Instead, optional values are represented by Option<T>, which is either Some(value) or None. You cannot access the inner value without explicitly handling both cases — the compiler refuses to compile code that skips the check. Rust’s unwrap() method panics on None, which is functionally equivalent to Go’s nil panic, but it requires an explicit opt-in. The debate in the Rust community around “is unwrap() acceptable in production code?” mirrors Go’s debate around whether recover() is an appropriate safety net. The key difference is that Rust makes the unsafe path explicit while Go makes it the default.

Kotlin uses ? in the type system: val user: User? is nullable, val user: User is non-null. Calling user.name on a nullable type is a compile error — you must use user?.name (safe call) or user!!.name (explicit assert that panics on null, like Go’s behavior). Java throws NullPointerException at runtime, identical in spirit to Go’s nil panic, but Java 14+ added helpful NPE messages that tell you exactly which reference was null on a chained call. Go’s stack trace gives you the line number but not which pointer on that line was nil. TypeScript with strictNullChecks makes null and undefined explicit in the type system — string | null is a different type from string, and the compiler forces you to narrow before accessing properties. C# introduced nullable reference types in C# 8.0 as an opt-in feature: string? vs string. The compiler emits warnings (not errors by default) for unguarded dereferences, which is a middle ground between Go’s silence and Rust’s refusal to compile.

Go’s recover() pattern for catching panics in HTTP handlers has a parallel in each language: Rust uses catch_unwind (rarely used outside FFI boundaries), Java uses try/catch, and Kotlin uses structured exception handling. The consensus across ecosystems is the same — catch at the boundary, fix the root cause, don’t use panic recovery as flow control.

Fix 1: Check for nil Before Dereferencing

Always check pointers for nil before accessing their fields:

// PANICS if user is nil
func getUsername(user *User) string {
    return user.Name  // panic: nil pointer dereference
}

// SAFE
func getUsername(user *User) string {
    if user == nil {
        return ""  // or return a default, or return an error
    }
    return user.Name
}

For nested structs, check each level:

type User struct {
    Name    string
    Address *Address
}

type Address struct {
    City string
}

// PANICS if user.Address is nil
func getCity(user *User) string {
    return user.Address.City
}

// SAFE
func getCity(user *User) string {
    if user == nil || user.Address == nil {
        return ""
    }
    return user.Address.City
}

Fix 2: Always Check Errors from Functions That Return Pointers

The most common source of nil pointer panics is ignoring error return values:

// PANICS — user is nil when FindUser returns an error
user, _ := db.FindUser(id)
fmt.Println(user.Name)  // panic if user is nil

// SAFE — check the error before using the result
user, err := db.FindUser(id)
if err != nil {
    return fmt.Errorf("finding user %d: %w", id, err)
}
fmt.Println(user.Name)  // Safe — user is not nil if err is nil

The contract in Go: if a function returns (*T, error), it should return either a valid pointer with nil error, or nil pointer with a non-nil error. Don’t trust the pointer until you’ve verified the error is nil.

// Function that follows the convention
func FindUser(id int) (*User, error) {
    var user User
    err := db.QueryRow("SELECT * FROM users WHERE id = $1", id).Scan(&user.ID, &user.Name)
    if err == sql.ErrNoRows {
        return nil, fmt.Errorf("user %d not found", id)
    }
    if err != nil {
        return nil, fmt.Errorf("querying user: %w", err)
    }
    return &user, nil
}

Common Mistake: Using _ to discard errors from functions that return pointers. Any _, _ or val, _ pattern that discards the error is a potential nil panic waiting to happen.

Fix 3: Initialize Maps Before Writing

A nil map causes a panic on write but not on read (reads return the zero value):

var scores map[string]int

// READ — safe, returns 0 (zero value for int)
val := scores["alice"]  // val = 0, no panic

// WRITE — PANICS
scores["alice"] = 100   // panic: assignment to entry in nil map

Fix — initialize the map:

// Option 1: make
scores := make(map[string]int)
scores["alice"] = 100  // Safe

// Option 2: map literal
scores := map[string]int{}
scores["alice"] = 100  // Safe

// Option 3: map literal with initial values
scores := map[string]int{
    "alice": 100,
    "bob":   85,
}

In structs, initialize maps in a constructor:

type UserStore struct {
    users map[string]*User
}

// Without constructor — users is nil
var store UserStore
store.users["alice"] = &User{}  // panic

// With constructor
func NewUserStore() *UserStore {
    return &UserStore{
        users: make(map[string]*User),  // Initialize the map
    }
}

store := NewUserStore()
store.users["alice"] = &User{}  // Safe

Fix 4: Handle the Nil Interface Trap

A nil interface and an interface holding a nil pointer are different:

type Animal interface {
    Sound() string
}

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

var dog *Dog = nil
var animal Animal = dog  // Interface holds a nil *Dog

// animal is NOT nil — it's an interface with a non-nil type but nil value
fmt.Println(animal == nil)  // false!
animal.Sound()              // panic: nil pointer dereference (inside Sound())

This trap is most common when returning interfaces:

// BUG — returns a non-nil interface holding a nil pointer
func getAnimal(sound string) Animal {
    var dog *Dog
    if sound == "woof" {
        dog = &Dog{}
    }
    return dog  // If dog is nil, this returns a non-nil interface!
}

animal := getAnimal("meow")
if animal != nil {  // This check passes! animal is not nil interface
    animal.Sound() // PANIC
}

// FIX — return nil interface explicitly
func getAnimal(sound string) Animal {
    if sound == "woof" {
        return &Dog{}
    }
    return nil  // Returns a true nil interface
}

Fix 5: Handle nil Receivers in Methods

A method can be called on a nil receiver if it’s a pointer receiver. If the method doesn’t check for nil, it panics when accessing fields:

type Node struct {
    Value int
    Next  *Node
}

// PANICS if n is nil
func (n *Node) GetValue() int {
    return n.Value  // panic if n == nil
}

// SAFE — explicitly handle nil receiver
func (n *Node) GetValue() int {
    if n == nil {
        return 0
    }
    return n.Value
}

// This pattern allows safe traversal of nil-terminated lists
func (n *Node) String() string {
    if n == nil {
        return "nil"
    }
    return fmt.Sprintf("%d -> %s", n.Value, n.Next.String())
}

This technique is used in the standard library (e.g., (*bytes.Buffer).Write handles nil safely).

Fix 6: Use the ok Pattern for Type Assertions

Type assertions on interface values panic if the interface is nil or the type doesn’t match:

var i interface{} = nil

// PANICS
s := i.(string)  // panic: interface conversion: interface is nil, not string

// SAFE — two-value form
s, ok := i.(string)
if !ok {
    fmt.Println("not a string")
    return
}
fmt.Println(s)

Same for map lookups — always use the ok form when existence matters:

m := map[string]*User{"alice": {Name: "Alice"}}

// RISKY — user might be nil if key doesn't exist
user := m["bob"]
fmt.Println(user.Name)  // panic — "bob" not in map

// SAFE
user, ok := m["bob"]
if !ok {
    fmt.Println("user not found")
    return
}
fmt.Println(user.Name)

Fix 7: Recover from Panics in HTTP Handlers

For HTTP servers, a nil pointer panic in one handler shouldn’t crash the entire server. Use recover in middleware:

func recoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("panic: %v\n%s", err, debug.Stack())
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/api/users", handleUsers)

    // Wrap with recovery middleware
    handler := recoveryMiddleware(mux)
    log.Fatal(http.ListenAndServe(":8080", handler))
}

Fix the underlying nil accessrecover is a safety net, not a solution. Find and fix the cause by reading the stack trace in the log.

Debugging nil Pointer Panics

Read the full stack trace. The panic output shows the exact line number and call chain:

goroutine 1 [running]:
main.getCity(...)
        /app/main.go:23 +0x18   ← Line 23 in main.go
main.handleRequest(...)
        /app/main.go:15 +0x28

Go to line 23. The dereference is there. Ask: “Which pointer on this line could be nil?”

Add nil checks progressively:

func getCity(user *User) string {
    fmt.Printf("user: %v\n", user)            // Debug
    fmt.Printf("user.Address: %v\n", user.Address)  // Debug
    return user.Address.City
}

Use dlv (Delve debugger) to catch panics:

dlv debug ./main.go
(dlv) catch panic
(dlv) continue
# Execution pauses at the panic with full state inspection

Still Not Working?

Check for concurrent nil writes. If multiple goroutines write to a pointer without synchronization, one goroutine might set it to nil while another reads it:

var user *User

go func() {
    user = nil  // Concurrent nil write
}()

if user != nil {
    fmt.Println(user.Name)  // Race condition: user might be nil here
}

Fix with a mutex or use sync/atomic for pointer operations.

Use go vet and the race detector:

go vet ./...
go test -race ./...
go run -race main.go

The race detector catches concurrent nil pointer races that are otherwise intermittent and hard to reproduce.

Use nilaway for static analysis. NilAway is a static analysis tool from Uber that detects potential nil dereferences at compile time. It tracks nil flow across function boundaries and reports paths where a nil pointer could reach a dereference without a guard. Run it alongside go vet in CI to catch issues before they reach production:

go install go.uber.org/nilaway/cmd/nilaway@latest
nilaway ./...

Check for nil in deferred function calls. A common subtle case: if you defer a method call on a variable that becomes nil later in the function, the deferred call executes with the nil value at function exit:

func process() error {
    conn, err := connect()
    if err != nil {
        return err
    }
    defer conn.Close()  // Safe — conn is non-nil here

    // But this is dangerous:
    var result *Result
    defer result.Cleanup()  // result is nil at defer registration AND at execution
    // ...
}

For related Go issues, see Fix: Go Goroutine Deadlock, Fix: Go panic: runtime error: index out of range, Fix: Go Context Deadline Exceeded, and Fix: Go Interface nil Panic.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles