Go Interfaces Can Hold Typed Nil Pointers
Understand why a Go interface containing a nil pointer is not nil, how assertions behave, and how to prevent non-nil error surprises.
A Go interface compares equal to nil only when it has neither a dynamic type nor a dynamic value. Assigning a typed nil pointer to an interface supplies a dynamic type, so the interface is non-nil even though the stored pointer is nil. This is most dangerous when a function accidentally returns a typed nil pointer as an error.
The rule is part of the language rather than a Go 1.27 change. The current Go specification’s interface examples distinguish a nil interface from an interface whose dynamic type is *T and whose dynamic value is nil.
An interface carries a dynamic type and value
Consider a pointer type that implements error:
package main
import "fmt"
type Problem struct {
Code string
}
func (p *Problem) Error() string {
if p == nil {
return "<nil Problem>"
}
return "problem: " + p.Code
}
func main() {
var p *Problem
var err error = p
fmt.Println(p == nil)
fmt.Println(err == nil)
got, ok := err.(*Problem)
fmt.Println(ok, got == nil)
}
It prints:
true
false
true true
Before the assignment, p has static type *Problem and value nil. After the assignment, err has static type error, dynamic type *Problem, and a nil dynamic value. The type assertion succeeds because the dynamic type really is *Problem; the asserted pointer is still nil.
The Go FAQ describes an interface value as a dynamic type and value pair. Only the pair with no dynamic type and no value is a nil interface.
The usual bug is a typed nil error return
This function appears to return no error for an empty code, but it does the opposite:
func validate(code string) error {
var problem *Problem
if code != "" {
problem = &Problem{Code: code}
}
return problem
}
The conversion to error happens at the return. Even when problem is nil, the result carries dynamic type *Problem, so validate("") != nil.
Return an untyped nil on the successful path instead:
func validate(code string) error {
if code == "" {
return nil
}
return &Problem{Code: code}
}
Named error results do not remove the risk. Assigning a typed nil pointer to a named error result creates the same non-nil interface. The reliable boundary is explicit: return nil when the operation succeeded, and construct the concrete error only on failure.
A nil receiver method may still be called
Because the interface is non-nil and its dynamic type implements error, calling err.Error() dispatches to (*Problem).Error. A method with a pointer receiver can be entered with a nil receiver. What happens next depends on its implementation.
The example checks p == nil and is safe. A method that immediately reads p.Code will panic. Go does not insert a general nil-receiver guard before the call.
Nil-safe receiver methods can be useful for a type whose contract deliberately permits them, but they do not make the containing interface nil. They can also hide the source of an unintended typed nil. For ordinary error types, preventing the bad return is clearer than teaching every method to tolerate it.
Assertions and type switches see the type
A two-result type assertion succeeds for a typed nil:
problem, ok := err.(*Problem)
if ok && problem == nil {
// err carries *Problem, but its stored pointer is nil.
}
A type switch likewise enters case *Problem, with a nil value bound in that case. Testing only ok or selecting a type-switch case answers a type question, not a nil-pointer question.
This behavior is useful when the zero pointer has deliberate meaning, but it is rarely a good public error contract. Callers conventionally use err == nil as the success test. Returning a typed nil violates that expectation even though the language permits it.
Reflection needs two checks
Generic diagnostics sometimes need to detect nil-capable values hidden inside any. Reflection can do that, but reflect.Value.IsNil is defined only for channels, functions, interfaces, maps, pointers, and slices. It also panics on an invalid zero Value, which is what reflect.ValueOf(nil) returns.
A guarded helper looks like this:
func nilLike(value any) bool {
if value == nil {
return true
}
v := reflect.ValueOf(value)
switch v.Kind() {
case reflect.Chan, reflect.Func, reflect.Map,
reflect.Pointer, reflect.Slice:
return v.IsNil()
default:
return false
}
}
The reflect.Value.IsNil documentation spells out both restrictions. Use a helper like this at reflection-heavy framework boundaries, not as a replacement for a precise API. “Nil-like” combines distinct states—a nil slice is often a usable empty collection, while a nil function cannot be called.
Design APIs around the interface contract
Prefer interface return types when callers need behavior from multiple implementations. Prefer concrete pointer returns when callers genuinely need to distinguish a nil pointer from a populated value. For an error result, preserve the universal convention: nil means success and non-nil means failure.
When reviewing a suspicious return, trace the conversion point:
- What is the concrete expression’s static type?
- Can that type have a nil value?
- Does assigning it to the interface attach a dynamic type?
- If a method is called, can its receiver safely be nil?
The key is not an implementation-specific “two-word interface” representation. The specification-level rule is enough: a nil interface has no dynamic type, and a typed nil supplies one. Return the untyped nil value on success, and the common if err != nil check works as intended.