Avoid Nil Panics

Problem

Nil pointer dereferences are one of the most common runtime panics in Go. They occur when you try to access a field or call a method on a nil pointer.

var user *User // nil pointer
name := user.Name // panic: runtime error: invalid memory address

This guide shows practical techniques to prevent nil panics in your code.

Solution Strategies

Design for Zero Values

Make your types work with their zero value to eliminate nil pointer issues.

When to use: Structs that can have sensible zero values.

// ❌ Requires initialization - nil pointer danger
type Buffer struct {
  data *[]byte // Pointer to slice - nil by default
  size int
}

func (b *Buffer) Write(p []byte) {
  if b.data == nil {
    // Must check for nil every time
    slice := make([]byte, 0)
    b.data = &slice
  }
  *b.data = append(*b.data, p...)
}

// ✅ Zero value works immediately
type Buffer struct {
  data []byte // Slice itself, not pointer - nil slice works with append!
  size int
}

func (b *Buffer) Write(p []byte) {
  // Safe - append handles nil slice
  b.data = append(b.data, p...)
  b.size += len(p)
}

// No constructor needed
var buf Buffer // Zero value ready to use
buf.Write([]byte("hello"))

More zero value examples:

// ✅ Mutex zero value works
type Counter struct {
  mu    sync.Mutex // Not *sync.Mutex!
  count int
}

func (c *Counter) Increment() {
  c.mu.Lock() // Safe - zero value Mutex works
  defer c.mu.Unlock()
  c.count++
}

// ✅ Map can check for nil
type Cache struct {
  data map[string]string
}

func (c *Cache) Get(key string) (string, bool) {
  // Safe - reading from nil map returns zero value
  val, ok := c.data[key]
  return val, ok
}

func (c *Cache) Set(key, val string) {
  if c.data == nil {
    c.data = make(map[string]string)
  }
  c.data[key] = val
}

Benefits:

  • No nil pointer panics at field access
  • Simpler API (no required constructors)
  • Follows Go idioms
  • Safer code by default

Validate Inputs at Function Entry

Check for nil parameters immediately at the start of functions.

When to use: Public APIs, critical business logic, functions that can’t handle nil.

// ❌ Panics deep in function - hard to debug
func ProcessOrder(order *Order) error {
  // Many lines of code...

  customer := order.Customer // panic if order is nil!
  // More code...

  total := order.Total // Would panic here too
  return nil
}

// ✅ Fail fast with clear error
func ProcessOrder(order *Order) error {
  if order == nil {
    return errors.New("order cannot be nil")
  }

  // Safe to use order
  customer := order.Customer
  total := order.Total
  return nil
}

// ✅ Validate nested fields too
func ProcessOrder(order *Order) error {
  if order == nil {
    return errors.New("order cannot be nil")
  }
  if order.Customer == nil {
    return errors.New("order.Customer cannot be nil")
  }
  if order.Items == nil || len(order.Items) == 0 {
    return errors.New("order must have items")
  }

  // All required fields validated
  return processValidOrder(order)
}

When NOT to validate:

// Don't validate in private functions with guaranteed callers
func (s *OrderService) processValidOrder(order *Order) error {
  // Private function - caller already validated
  // No need to re-check
  return s.db.Save(order)
}

Use Pointer Receivers Carefully

Avoid nil receiver panics by checking in methods or using value receivers.

// ❌ Panics if receiver is nil
type Counter struct {
  count int
}

func (c *Counter) Increment() {
  c.count++ // panic if c is nil!
}

var c *Counter // nil pointer
c.Increment() // panic!

// ✅ Check for nil receiver
func (c *Counter) Increment() {
  if c == nil {
    return // Or handle appropriately
  }
  c.count++
}

// ✅ Or use value receiver when possible
type Counter struct {
  count int
}

func (c Counter) Value() int {
  return c.count // Safe - value receiver can't be nil
}

// ✅ Nil receiver as valid state
type Tree struct {
  value int
  left  *Tree
  right *Tree
}

func (t *Tree) Sum() int {
  if t == nil {
    return 0 // Nil tree has sum of 0
  }
  return t.value + t.left.Sum() + t.right.Sum()
}

// Elegant recursion with nil receivers
var tree *Tree // nil
sum := tree.Sum() // Returns 0, no panic

When to use value vs pointer receivers:

// Value receiver for small, immutable types
type Point struct {
  X, Y int
}

func (p Point) Distance() float64 { // Value - safe from nil
  return math.Sqrt(float64(p.X*p.X + p.Y*p.Y))
}

// Pointer receiver when you need to modify
type Account struct {
  balance int
}

func (a *Account) Deposit(amount int) {
  if a == nil {
    panic("cannot deposit to nil account") // Or return error
  }
  a.balance += amount
}

Return Zero Values Instead of Nil

Return zero values for better safety when nil has no semantic meaning.

// ❌ Returns nil - caller must check
func GetDefaultConfig() *Config {
  return nil // Caller will panic if they don't check
}

// Caller must remember to check
config := GetDefaultConfig()
if config != nil {
  config.Apply() // Could still panic if forgotten
}

// ✅ Return zero value
func GetDefaultConfig() Config {
  return Config{} // Zero value, not nil
}

// Safe to use
config := GetDefaultConfig()
config.Apply() // No panic possible

// ✅ Return zero value with indication
func FindUser(id string) (User, bool) {
  user, ok := db[id]
  if !ok {
    return User{}, false // Zero value + bool
  }
  return user, true
}

// Caller knows when value is valid
user, found := FindUser("123")
if found {
  fmt.Println(user.Name) // Safe
}

When nil is semantically meaningful:

// Nil means "no value" - use pointer
func FindUser(id string) (*User, error) {
  user, err := db.Query(id)
  if err != nil {
    return nil, err // nil with error = lookup failed
  }
  if user == nil {
    return nil, nil // nil with no error = not found
  }
  return user, nil
}

Use Safe Navigation Patterns

Check for nil before accessing nested fields.

// ❌ Multiple potential panics
func GetUserCity(user *User) string {
  return user.Address.City // panic if user or Address is nil
}

// ❌ Nested nil checks - hard to read
func GetUserCity(user *User) string {
  if user != nil {
    if user.Address != nil {
      return user.Address.City
    }
  }
  return ""
}

// ✅ Early return pattern
func GetUserCity(user *User) string {
  if user == nil {
    return ""
  }
  if user.Address == nil {
    return ""
  }
  return user.Address.City
}

// ✅ Helper function
func GetUserCity(user *User) string {
  if addr := getUserAddress(user); addr != nil {
    return addr.City
  }
  return ""
}

func getUserAddress(user *User) *Address {
  if user == nil {
    return nil
  }
  return user.Address
}

// ✅ Builder pattern for safe chaining
type UserQuery struct {
  user *User
}

func NewUserQuery(user *User) *UserQuery {
  return &UserQuery{user: user}
}

func (q *UserQuery) GetAddress() *AddressQuery {
  if q.user == nil {
    return &AddressQuery{addr: nil}
  }
  return &AddressQuery{addr: q.user.Address}
}

type AddressQuery struct {
  addr *Address
}

func (q *AddressQuery) GetCity() string {
  if q.addr == nil {
    return ""
  }
  return q.addr.City
}

// Safe chaining
city := NewUserQuery(user).GetAddress().GetCity()

Initialize Slices and Maps Properly

Avoid nil slice/map panics by initializing them correctly.

// ❌ Nil map - panic on write
var users map[string]*User
users["john"] = &User{} // panic: assignment to entry in nil map

// ✅ Initialize map
users := make(map[string]*User)
users["john"] = &User{} // Safe

// ✅ Nil slice is OK for read and append
var items []Item // nil slice
items = append(items, Item{}) // Safe - append handles nil
for _, item := range items {} // Safe - iterating nil slice is fine

// ❌ Nil slice panic on index assignment
var items []Item
items[0] = Item{} // panic: index out of range

// ✅ Pre-allocate if you need indexing
items := make([]Item, 10)
items[0] = Item{} // Safe

// ✅ Check before indexing
func GetFirst(items []Item) (Item, bool) {
  if len(items) == 0 {
    return Item{}, false
  }
  return items[0], true
}

Use Constructors for Complex Types

Provide constructors that guarantee proper initialization.

// ❌ Manual initialization - error-prone
type Database struct {
  conn   *sql.DB
  cache  map[string]interface{}
  logger *log.Logger
}

// User might forget to initialize cache
db := &Database{
  conn:   sqlConn,
  logger: logger,
  // Forgot cache - will panic on use
}

// ✅ Constructor guarantees initialization
func NewDatabase(conn *sql.DB, logger *log.Logger) *Database {
  return &Database{
    conn:   conn,
    cache:  make(map[string]interface{}), // Guaranteed initialized
    logger: logger,
  }
}

// Safe to use
db := NewDatabase(sqlConn, logger)
db.cache["key"] = "value" // No panic

// ✅ Functional options pattern
type Database struct {
  conn      *sql.DB
  cache     map[string]interface{}
  logger    *log.Logger
  timeout   time.Duration
}

type Option func(*Database)

func WithLogger(logger *log.Logger) Option {
  return func(db *Database) {
    db.logger = logger
  }
}

func WithTimeout(timeout time.Duration) Option {
  return func(db *Database) {
    db.timeout = timeout
  }
}

func NewDatabase(conn *sql.DB, opts ...Option) *Database {
  db := &Database{
    conn:    conn,
    cache:   make(map[string]interface{}), // Guaranteed
    timeout: 5 * time.Second, // Sensible default
  }

  for _, opt := range opts {
    opt(db)
  }

  return db
}

// Flexible and safe
db := NewDatabase(sqlConn,
  WithLogger(logger),
  WithTimeout(10 * time.Second),
)

Defensive Programming in Method Chains

Make method chains nil-safe to prevent cascade failures.

// ❌ Chain breaks on any nil
func (u *User) GetAddress() *Address {
  return u.Address // panic if u is nil
}

func (a *Address) GetCity() string {
  return a.City // panic if a is nil
}

// Unsafe chaining
city := user.GetAddress().GetCity() // Double panic potential

// ✅ Nil-safe chain
func (u *User) GetAddress() *Address {
  if u == nil {
    return nil
  }
  return u.Address
}

func (a *Address) GetCity() string {
  if a == nil {
    return ""
  }
  return a.City
}

// Safe chaining
city := user.GetAddress().GetCity() // Returns "" if any nil

// ✅ Alternative: Return error
func (u *User) GetAddress() (*Address, error) {
  if u == nil {
    return nil, errors.New("user is nil")
  }
  if u.Address == nil {
    return nil, errors.New("user has no address")
  }
  return u.Address, nil
}

Putting It All Together

When you’re ready to make your code nil-safe, start with your type designs. Review each struct and ask whether fields should be pointers or values. Use pointers only when you need to represent absence (nil) or when the value is large enough that copying would hurt performance. For everything else, use values - their zero values prevent nil panics entirely.

Next, examine your public API functions. Add nil checks at the start of any function that accepts pointer parameters and cannot handle nil safely. These checks catch problems at the API boundary rather than deep in your implementation. Return clear errors that identify exactly which parameter was nil, making debugging trivial.

Review your method receivers. If you use pointer receivers, decide whether nil is a valid receiver state for each method. For recursive structures like trees or linked lists, nil receivers often make elegant sense. For most business logic types, nil receivers indicate bugs and should panic with clear messages.

Look at your constructors. Do they initialize all fields properly? Add constructors for any complex type that needs multiple fields initialized together. This prevents users from creating instances with nil maps or other unsafe state. Use the functional options pattern when you have many optional configuration fields.

Finally, audit your method chains. Any chain of pointer methods creates multiple failure points. Either make the methods nil-safe by returning zero values, or break the chain into separate calls with error checking. This makes the error handling explicit and prevents mysterious panics deep in chains.

Common Mistakes to Avoid

Don’t forget to initialize maps:

// ❌ Nil map panic
var cache map[string]string
cache["key"] = "value" // panic!

// ✅ Initialize first
cache := make(map[string]string)
cache["key"] = "value" // Safe

Don’t assume interface{} is not nil:

// ❌ Interface holding nil pointer appears non-nil
var ptr *int = nil
var iface interface{} = ptr

if iface != nil {
  // This runs! Interface is not nil even though it holds nil
  fmt.Println(iface) // prints: <nil>
}

// ✅ Check underlying value
if iface != nil && reflect.ValueOf(iface).IsNil() {
  // This won't run
}

Don’t return typed nil:

// ❌ Returns typed nil - appears non-nil to caller
func GetUser() *User {
  var user *User = nil
  return user
}

if u := GetUser(); u != nil {
  // Runs even though u is nil!
}

// ✅ Return untyped nil or error
func GetUser() (*User, error) {
  return nil, errors.New("not found")
}

Summary

Preventing nil panics in Go requires designing types and APIs that minimize nil pointer usage while handling unavoidable cases safely. Start by making your zero values useful - use value types instead of pointers whenever the field doesn’t need to represent absence. Slices, maps (for reading), and synchronization primitives work perfectly as values, eliminating entire categories of nil pointer bugs.

When you must use pointers, validate them at function entry points rather than checking throughout your implementation. Fail fast with clear error messages that identify exactly what was nil. This makes bugs obvious and easy to fix rather than mysterious panics deep in call stacks.

Design your method receivers thoughtfully. Use value receivers for small types and immutable operations - they cannot be nil. When you need pointer receivers for mutation or large types, decide whether nil is a valid receiver state. For recursive structures like trees, nil receivers enable elegant recursion. For business logic, nil receivers usually indicate bugs and should panic clearly.

Provide constructors for complex types that require multiple fields to be initialized together. This prevents users from creating half-initialized instances with nil maps or slices they’ll panic on later. The functional options pattern works well when you have many optional configuration parameters.

Return zero values instead of nil when absence has no semantic meaning. A zero-value struct is often safer and more convenient than a nil pointer that callers must check. When nil does mean something specific (like “not found” vs “error”), use it deliberately with clear documentation.

These techniques work together to create code that’s resilient to nil pointer errors by design rather than through defensive checking scattered everywhere. The result is safer, clearer code that fails fast when problems occur and works correctly by default.

Related Content

Last updated