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 ./...
Comments