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 addressThis 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 panicWhen 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" // SafeDon’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.