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