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.
func (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
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.
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.
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.
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