nil Receivers and Nil Safety

A method with a pointer receiver can run on a nil pointer, which lets nil stand for an empty value instead of a crash.

Syntaxfunc (p *T) M() { if p == nil { /* the empty case */ } ... }

A method with a pointer receiver can be called on a nil pointer. This surprises people, and it is not an accident. The call does not read anything through the pointer; it only passes the pointer in as the receiver. Inside the method, the receiver is simply a pointer whose value is nil, and you can test it with if t == nil.

Why that is useful

It lets nil be a meaningful value rather than a hole in your program. A nil *Tree is an empty tree. A nil *Logger is logging switched off. A nil *Config is "nothing was supplied, use the defaults". The check lives once inside the method instead of at every call site, and recursive code gets much shorter: a tree walk needs no special case for a missing branch.

When it panics

The method must not touch a field of a nil receiver. return t.Value on a nil t stops the program with "invalid memory address or nil pointer dereference". So a nil receiver is safe only for methods written to expect it, and that is worth saying in a comment on the type.

A value receiver is never nil-safe. s.IsDebug() where IsDebug has the receiver (s Settings) is shorthand for the longhand (*s).IsDebug(). The copy happens as part of the call, before the body starts, so the program stops even if the body never reads a field.

Nil values, not just pointers

The same idea covers nil slices and nil maps as receivers. A method on a named slice type can call len and range on a nil receiver, because both work on a nil slice.

Example

Example · go
package main

import "fmt"

// Tree is one node of a binary tree. A nil *Tree is a valid empty tree,
// and every method must handle it.
type Tree struct {
	Value       int
	Left, Right *Tree
}

// Sum adds up the whole tree. The nil check replaces every other check.
func (t *Tree) Sum() int {
	if t == nil {
		return 0
	}
	return t.Value + t.Left.Sum() + t.Right.Sum()
}

func main() {
	var empty *Tree // nil pointer
	fmt.Println(empty.Sum())

	t := &Tree{
		Value: 5,
		Left:  &Tree{Value: 3},
		Right: &Tree{Value: 2},
	}
	// t.Left.Right is nil, and calling Sum on it is still safe.
	fmt.Println(t.Sum(), t.Left.Right.Sum())
}

// Output:
// 0
// 10 0

When to use it

  • Let a nil *Tree stand for an empty tree so recursive methods need no extra cases.
  • Make an optional *Config safe to query without a nil check at every call site.
  • Return false from an Enabled method when the feature struct was never created.
  • Review a pointer-receiver method to decide whether nil is allowed, and comment it.

More examples

One nil-safe method and one that is not

Nil safety is a property of each method, not of the type. This pattern lets an optional dependency be left out entirely: code calls l.Enabled() first and only reaches the field-reading methods when it is true. If you intend nil to be allowed, say so in a comment above the type.

Example · go
package main

import "fmt"

type Logger struct {
	Prefix string
}

// Enabled touches no field, so a nil receiver is fine.
func (l *Logger) Enabled() bool {
	return l != nil
}

// Tag reads a field, so a nil receiver stops the program.
func (l *Logger) Tag() string {
	return l.Prefix
}

func main() {
	var l *Logger
	fmt.Println(l.Enabled())
	// l.Tag() here would stop the program with
	// "invalid memory address or nil pointer dereference".

	l = &Logger{Prefix: "app"}
	fmt.Println(l.Enabled(), l.Tag())
}

// Output:
// false
// true app

A value receiver cannot be nil-safe

If a type is going to be passed around as a pointer that may be nil, every method on it needs a pointer receiver. In the standard library, crypto/tls documents (*Config).Clone as returning nil when the receiver is nil, which is this pattern in production code.

Example · go
package main

import "fmt"

type Settings struct {
	Debug bool
}

// Value receiver: the call copies *s before the body starts.
func (s Settings) IsDebug() bool {
	return s.Debug
}

// Pointer receiver: the method itself decides what nil means.
func (s *Settings) IsDebugSafe() bool {
	if s == nil {
		return false // nothing configured, so not in debug mode
	}
	return s.Debug
}

func main() {
	var s *Settings
	fmt.Println(s.IsDebugSafe())
	// fmt.Println(s.IsDebug()) would stop the program, because
	// s.IsDebug() means (*s).IsDebug() and *s cannot be read.

	s = &Settings{Debug: true}
	fmt.Println(s.IsDebug(), s.IsDebugSafe())
}

// Output:
// false
// true true

A nil slice receiver works without any check

Slices and maps as receivers inherit the nil behaviour of the built-in operations: len, range and append all handle a nil slice, so the zero value of Stack is usable with no constructor at all. Note that Push has to return the new Stack, since a value receiver cannot replace the caller's variable.

Example · go
package main

import "fmt"

// Stack is a named slice type. Its zero value, nil, is an empty stack.
type Stack []int

func (s Stack) Len() int {
	return len(s) // len of a nil slice is 0
}

// Push returns the new stack, because a value receiver cannot change the caller.
func (s Stack) Push(v int) Stack {
	return append(s, v) // append works on a nil slice
}

func main() {
	var s Stack // nil
	fmt.Println(s.Len())
	s = s.Push(1).Push(2)
	fmt.Println(s.Len(), s)
}

// Output:
// 0
// 2 [1 2]

Discussion

  • Be the first to comment on this lesson.