skip to content

A Go CLI panics with a nil pointer dereference on a line calling a func-typed struct field. How do you diagnose and fix it?

level: seniorimportance: nice to knowfreq 34%

answer

  1. the message lies about which nil it was
  2. look at the signal line, not just the text
  3. the callee left no frame behind
  4. an optional hook nobody assigned
  5. default it in the constructor

basics

~20 s

Calling a nil function value produces the same message as a nil pointer dereference, but the signal line shows pc=0x0 and the callee has no stack frame. The fix is to install no-op defaults in the constructor, or to guard the call with a nil check.

solid answer

~50 s

Calling a nil func value panics with `runtime error: invalid memory address or nil pointer dereference` - the same text a nil pointer dereference produces, which is why it gets misdiagnosed. Two things in the dump separate them: the signal line typically shows `pc=0x0`, meaning control was transferred to address zero rather than faulting inside a real instruction, and the topmost user frame is the caller, because the callee never existed. Open that file and line: if the expression calls a hook field, a map entry or a package variable of a function type, the value was simply never assigned. The usual cause is a struct with optional callbacks built as a bare literal, or decoded from input where that section was absent, so the field kept the zero value of a func type. Prefer fixing it in the constructor by installing no-op functions for every unset hook, so no call site needs a guard; use `if r.OnStart != nil` only where callers legitimately build the struct themselves.

code

go · 16 lines
go
type runner struct {
	OnStart func(name string) error // optional hook
}

func (r *runner) run(name string) error {
	if r.OnStart != nil { // without this, a nil func value panics
		if err := r.OnStart(name); err != nil {
			return err
		}
	}
	return nil
}

func newRunner() *runner { // better: the field is never nil
	return &runner{OnStart: func(string) error { return nil }}
}

go deeper

for a junior

Know that a function-typed field starts out nil and that calling it panics. If you build a struct with a literal, check which callback fields you left unset.

for a middle

Read the dump properly: the panic text is shared with a nil dereference, so use the signal line and the missing callee frame to tell them apart, then map the reported line back to the unassigned field.

for a senior

Fix the class, not the instance - mandatory constructors that install no-op hooks, a documented statement about whether the zero value is usable, and a test that runs the rare branch on a bare literal.

for a principal

Set the convention for types other teams construct: unexported fields plus a constructor, or a documented usable zero value. Leaving it to each caller guarantees a production panic in the branch nobody exercises.

## The symptom and what it actually means Calling a nil function value in Go does not fail politely. It jumps to address zero, the OS delivers a fault, and the Go runtime reports it as: ``` panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x0] ``` Note that this is **the same message** you get from dereferencing a nil pointer. The panic text alone does not tell you which of the two happened, and that is why people misdiagnose it: they go looking for a nil struct pointer on the line, find the pointer is fine, and stall. ## Reading the trace Three things in the dump narrow it down quickly. 1. **`pc=0x0` in the signal line.** The program counter is the address the CPU was told to execute. For a nil *pointer* dereference the pc is a real code address inside your function - the instruction that did the load. A pc of zero means control was transferred *to* address zero, which is what calling a nil func value does. `addr=0x0` appears in both cases and is not the discriminator; `pc=0x0` is. 2. **There is no frame for the callee.** The topmost user frame in the goroutine dump is the *caller* - the function containing the call expression - with its file and line. The function that "should" have run has no frame, because it never existed. 3. **The line number points at a call, not at a field access.** Open the file at that line: if the expression is `x.Hook(...)`, `fn(...)` or `cfg.OnStart(...)`, and the value being called comes from a struct field, a map lookup, or a package-level variable, you are almost certainly calling a nil func value. ## The shape that produces it Optional callbacks. A CLI or a library type carries hooks that most callers never set: ```go type runner struct { OnStart func(name string) error // optional OnDone func(name string, err error) } ``` Any construction that does not go through the constructor - a bare `runner{}`, a struct literal that sets some fields, a config decoded from a file where the section was absent - leaves those fields as the zero value of a func type, which is nil. The code path that eventually calls `r.OnStart(name)` may be rare (an error branch, a `--verbose` flag, a subcommand nobody runs in tests), which is exactly why it reaches production. ## The fixes, in the order to prefer them 1. **Default the field at construction.** Have `newRunner` install no-op functions for every hook the caller did not supply. Now the field is never nil, no call site needs a guard, and there is one place to read to understand the behaviour. This is the version an engineer onboarding to the codebase can follow. 2. **Guard at the call site** - `if r.OnStart != nil { ... }` - when the type is legitimately used as a bare struct literal and you cannot force construction through a function. It is correct, but it is a guard you must remember at every call site, and forgetting one is the bug you just fixed. 3. **Make the hook an interface with a no-op implementation**, when there are several related callbacks. Beware: an interface field's zero value is also nil, and calling a method on a nil interface panics too, so this only helps if the constructor installs the no-op implementation. Do **not** "fix" it by making the whole call site defensive against everything nil. The lesson is narrower and more useful: a func-typed field is nil until someone assigns it, and Go gives you no compile-time help. ## Preventing the class, not the instance - Make the zero value either usable or unusable *on purpose*, and say which in the type's doc comment. - Keep constructors mandatory for types with hooks: unexported fields plus an exported `New...` means a caller cannot build a half-initialised value. - Add a test that exercises the rarely-taken branch with a zero-value struct. A single `runner{}.run("x")` test would have caught this before release. - When reviewing, treat every `SomeStruct{...}` literal of a hook-carrying type as a question: which hooks did this literal leave nil, and is any of them called?

  • How does the trace of a nil func call differ from a nil pointer dereference?
    The panic text is identical. The signal line differs: a nil dereference faults at a real instruction inside your function, so the program counter is a genuine code address, while calling a nil func value transfers control to address zero and the pc is reported as zero. The dump also shows no frame for the function that was supposed to run - the topmost user frame is the caller.
  • Why is defaulting the hook in a constructor better than guarding at the call site?
    The guard has to be repeated at every call site and the compiler will not remind you of the one you missed - which is the bug you just fixed, in a different branch. Installing a no-op in the constructor makes the field non-nil once, in a place a newcomer reads first, and leaves the call sites plain.
  • Would a test have caught this?
    Yes, if it exercised the rarely-taken branch with a zero-value struct - a single test that builds the type as a bare literal and runs the path that calls the hook. Hook fields fail in production precisely because tests build the type through the helper that sets everything.
  • Does declaring the hook as an interface instead of a func type avoid the problem?
    Not by itself. The zero value of an interface field is also nil, and calling a method on a nil interface panics too. It helps only if the constructor installs a no-op implementation, which is the same discipline the func-typed field needed.

saying these in an interview costs you the question

  • Insists the panic must be a nil struct pointer on that line
  • Believes calling a nil func value returns zero results
  • Adds nil checks everywhere instead of defaulting the field once
  • Assumes an interface-typed hook is nil-safe
  • Reads only the panic text and ignores the signal line and frames