skip to content

Why can a Go method call on a nil *T pointer receiver work, and when does it panic?

level: middleimportance: should knowfreq 45%

answer

  1. the receiver is an argument
  2. nothing at the call site reads through it
  3. the body decides whether it survives
  4. a value receiver needs a copy
  5. guard at the top, not at every caller

basics

~20 s

A pointer receiver is just the method's first argument, and nil is a legal value for it, so the call itself never dereferences anything. It panics only if the body reads through the pointer - or immediately if the method has a value receiver.

solid answer

~60 s

A method with a pointer receiver compiles to a function taking that pointer as an extra first parameter. Calling it passes the pointer by value, so a nil `*T` is a legal argument and control reaches the body with the receiver equal to nil. The panic comes from the body: the first field access or dereference on a nil pointer raises `runtime error: invalid memory address or nil pointer dereference`. A method that checks `if t == nil` first can return a sensible default and never panic, which is why patterns like `func (n *node) size() int { if n == nil { return 0 } ... }` are idiomatic. Two cases still panic: a value-receiver method called through a nil pointer, because the compiler must dereference to make the copy, and a method call on a nil interface value, because there is no dynamic type to dispatch through. Supporting a nil receiver is an API promise - document it on the type, and make it hold for every exported method.

code

go · 11 lines
go
type options struct{ verbose bool }

func (o *options) Verbose() bool {
	if o == nil {
		return false
	}
	return o.verbose
}

var o *options // the flag block was never parsed
fmt.Println(o.Verbose()) // prints: false

go deeper

for a junior

Know that a method can run with a nil pointer receiver and that the panic comes from touching a field. Recognise the if t == nil { return ... } guard at the top of a method as deliberate, not dead code.

for a middle

Explain the mechanism: the receiver is compiled as the first parameter, so the call passes a pointer rather than dereferencing one. Contrast pointer and value receivers, and say why only the former survives nil.

for a senior

Judge when to use it. Recursive structures and optional configuration are good fits; treat nil-receiver support as a documented contract covering every exported method, and reject a type that tolerates nil in three methods and panics in the fourth.

for a principal

Decide the house rule. Either a type's nil pointer is usable and that is stated on the type, or constructors are mandatory and the zero pointer is a bug - a codebase that mixes both trains readers to guess.

## A method is a function with an extra first argument When you declare `func (o *options) Verbose() bool`, the compiler produces something with the shape `func Verbose(o *options) bool`. The receiver is an ordinary parameter of type `*options`, and `nil` is an ordinary, legal value of that type. Nothing at the **call site** dereferences the pointer: the call passes the pointer by value, exactly as it would pass any other argument. So `var o *options; o.Verbose()` enters the method body with `o == nil`. Whether it survives depends entirely on what the body does. ```go type options struct{ verbose bool } func (o *options) Verbose() bool { if o == nil { return false // an absent options block means "not verbose" } return o.verbose } ``` Call that on a nil `*options` and it returns `false`. Delete the guard and the same call panics with `runtime error: invalid memory address or nil pointer dereference`, because `o.verbose` reads a field at offset 0 from address 0. ## When it does panic 1. **The body dereferences the receiver** - reading or writing a field, calling another method that does, or passing `*o` anywhere. This is the common case, and it is the reason nil-receiver methods must be written deliberately rather than discovered by accident. 2. **The method has a value receiver.** `func (o options) Name() string` called through a nil `*options` panics *before* the body runs: the compiler inserts a dereference to produce the copy that the value receiver requires. A pointer-receiver method tolerates a nil pointer; a value-receiver method never can. 3. **The call goes through a nil interface value.** If an interface variable holds no dynamic type at all, there is no method table to dispatch through and any method call on it panics. (This is different from an interface that holds a nil pointer of a concrete type - that one does dispatch, and lands in the method body with a nil receiver, exactly as above.) ## Why the pattern is worth knowing rather than just worth avoiding Nil-receiver methods let a type make its **nil pointer a usable value**, in the same spirit as a nil slice being a usable empty list. The classic shapes: - Recursive data structures: `func (n *node) size() int { if n == nil { return 0 }; return 1 + n.left.size() + n.right.size() }`. The nil check at the top removes the nil check at every call site, including the recursive ones. - Optional configuration: a `*options` field that is absent from the parsed input stays nil, and accessor methods answer with defaults instead of forcing every caller to write `if opts != nil && opts.verbose`. - A no-op implementation: a nil `*logger` whose methods return immediately, so callers never guard. ## Why the pattern is also dangerous It is **invisible at the call site**. A reader who sees `o.Verbose()` cannot tell whether a nil `o` is supported; only the method body says so, and only for that one method. If a type supports a nil receiver, that is an API promise and it belongs in the doc comment on the type - and it must hold for *every* exported method, because callers will not check which ones opted in. A type that tolerates nil in three methods and panics in the fourth is worse than one that never tolerates it, because the panic arrives in production rather than in the first test. The other cost is that it hides the "I never initialised this" bug. A nil `*options` that answers `false` reads the same as a real one that was configured with verbosity off. When the distinction matters, an explicit constructor that always returns a non-nil value with defaults filled in is easier for a newcomer to reason about than a type that silently works when it was never built. ## What an interviewer is listening for That you can say *why* it works - the receiver is an argument, not a dereference - rather than just that it does; that you know the value-receiver case behaves differently; and that you treat "nil receiver supported" as a documented part of the type's contract rather than an accident that happens to compile.

  • Why does a value-receiver method panic when called through a nil pointer?
    A value receiver takes a copy of the pointed-to value, so the compiler inserts a dereference at the call site to produce that copy. Dereferencing a nil pointer panics before the body ever runs. A pointer-receiver method inserts no such dereference, which is why only it can tolerate a nil receiver.
  • What happens if you call a method on an interface variable that holds nothing at all?
    It panics. A nil interface value is a pair with no dynamic type and no value, so there is no method table to dispatch through. That is different from an interface holding a nil pointer of a concrete type, where dispatch succeeds and the method body runs with a nil receiver.
  • When is supporting a nil receiver a bad idea?
    When it hides an initialisation bug. A nil `*options` that answers "not verbose" is indistinguishable from a real one configured that way, so a forgotten constructor call never surfaces. It is also dangerous when only some methods tolerate nil - callers cannot tell which, so the promise must cover every exported method or none.

saying these in an interview costs you the question

  • Says any method call on a nil pointer panics immediately
  • Thinks the runtime checks the receiver before dispatch
  • Believes a value receiver behaves like a pointer receiver here
  • Confuses a nil pointer receiver with a nil interface value
  • Treats nil-receiver support as an accident rather than a documented contract