Lesson 7 · Methods, interfaces & generics

The nil-interface gotcha

When err != nil is true and there's no error. The famous trap.

Your win: explain why a non-nil interface can hold a nil pointer, recognise the "typed nil error" bug, and state the fix. This is the most-asked Go interview gotcha — nail it and you sound like you understand the runtime.

An interface is a (type, value) pair

Under the hood, an interface value holds two words: a dynamic type and a value.1 The rule everything hinges on:

The rule An interface equals nil only when both its type and its value are nil. If the type slot is set — even with a nil value — the interface is not nil.

The bug: returning a typed nil as an error

func doThing() error {
    var e *MyError = nil       // a nil *MyError pointer
    return e                   // ⚠️ assigned into the error interface
}

err := doThing()
if err != nil {               // ✅ TRUE — even though "there is no error"!
    // the interface holds (type=*MyError, value=nil) → type slot set → not nil
}
returning e (*MyError = nil) as an error interface ┌───────────────┬───────────────┐ │ type: *MyError│ value: nil │ type slot is SET → interface != nil └───────────────┴───────────────┘ returning literal nil as an error interface ┌───────────────┬───────────────┐ │ type: nil │ value: nil │ both nil → interface == nil ✅ └───────────────┴───────────────┘

The classic form: a function declares a concrete error type, does its work, and return errVar where errVar is a nil pointer. Callers' if err != nil fires anyway, and everyone loses an afternoon.2

The fix — and why your repo is already safe

Return the literal nil for the interface on success; never return a typed nil pointer where an interface is expected. Our handlers do this naturally — success paths return an untyped nil:

internal/spike/modules/email/controller/grpc/email_modifier_send_email.go
return resp, nil                          // ✅ literal nil → interface truly nil
return nil, status.Error(codes.Internal, ...) // error path returns a real error
Why our code sidesteps it Our functions return the interface type error directly and hand back a bare nil on success — never a *someError that happens to be nil. The trap bites when you have a helper returning a concrete pointer type and you funnel it into an error return without a nil check. Keep the error variable typed as error, not *MyError.
Generalise it This isn't only about errors — it's true for any interface. A var w io.Writer = (*bytes.Buffer)(nil) is non-nil. Whenever you assign a concrete pointer into an interface, the interface carries that type forever.
Read this next

Go FAQ — "Why is my nil error value not equal to nil?"

The official explanation, straight from the source. Then 100 Go Mistakes covers typed-nil receivers in depth.

go.dev/doc/faq#nil_error
100 Go Mistakes (nil receivers)

Check yourself (from memory)

Q1. An interface value equals nil only when…

Both slots must be nil. A set type slot (even with a nil value) makes the interface non-nil.

Q2. A func returns a nil *MyErr as an error. The caller's err != nil is…

The interface holds (*MyErr, nil) — type set, so non-nil. The bug that eats afternoons.

Q3. To avoid the trap, a function returning error should…

Hand back the untyped literal nil — our repo's return resp, nil. Never funnel a concrete nil pointer into it.
Explain how return e (a nil *MyError) breaks if err != nil.
recall, then click to reveal
The return type is the error interface. Assigning a nil *MyError gives the interface a non-nil type slot (*MyError) with a nil value. An interface is nil only if both are nil, so it's non-nil — the caller's if err != nil fires. Fix: return the literal nil, and keep the variable typed as error, not *MyError.
Want to see how this interacts with typed-nil method calls (calling a method on a nil receiver — sometimes fine, sometimes a panic)? Ask me.

1. The Go Blog — The Laws of Reflection (interface = type + value).

2. Go FAQ — Why is my nil error value not equal to nil?