LEARN · DEBUGGING GUIDE

Golang Interface Nil vs Pointer Nil: Why They're Not Equal and How to Debug

An interface in Go is nil only when both its type and value are nil. A pointer typed nil inside an interface makes the interface non-nil, causing nil checks to fail silently.

IntermediateGo7 min read

What this usually means

The classic Go gotcha: an interface value is a two-word pair (type, value). When you assign a nil pointer of a concrete type (e.g., *MyStruct(nil)) to an interface variable, the interface's type word is set to *MyStruct and its value word is nil. The interface itself is NOT nil because the type word is non-nil. So a comparison like `if err != nil` evaluates to true (the interface is not nil), even though the underlying pointer is nil. This usually happens when functions return a named error variable or a pointer type that is nil, wrapped in an interface return type.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Check the concrete type of the interface with `fmt.Printf("%T\n", iface)` or `reflect.TypeOf(iface)`. If it shows a non-nil type, the interface is not nil.
  • 2Print the interface value with `%#v` to see the underlying type and pointer: `fmt.Printf("%#v\n", iface)`
  • 3Use `reflect.ValueOf(iface).IsNil()` to check if the underlying pointer is nil, but be careful: if the interface itself is nil, it panics.
  • 4Add a debug assertion: `if iface != nil && reflect.ValueOf(iface).IsNil() { log.Println("interface is non-nil but holds nil pointer") }`
  • 5Inspect the function return: ensure that a nil pointer of a concrete type is not returned directly as an interface. Instead, return nil interface explicitly.
  • 6Run `go vet` on your code: it may catch some cases of comparing interface with nil when the concrete type is a pointer.
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchFunctions that return an interface type (e.g., `error`, `io.Reader`) and use a named return variable of a concrete pointer type
  • searchCode that assigns a pointer variable (e.g., `var p *MyStruct`) to an interface variable without explicit nil check
  • searchJSON unmarshaling: `json.Unmarshal` into a struct with interface fields
  • searchFactory functions or constructors that return interfaces
  • searchAny place where a nil pointer is cast to an interface, e.g., `var err error = (*MyError)(nil)`
  • searchDeferred functions that check for nil interface before calling methods on it
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningReturning a named pointer variable that is nil, but the function signature returns an interface: the interface gets the non-nil type info.
  • warningAssigning a nil pointer of a concrete type to an interface variable directly, e.g., `var err error = (*MyError)(nil)`
  • warningJSON unmarshaling into a pointer field: if the JSON field is missing, the pointer stays nil, but the interface field (if any) is non-nil.
  • warningUsing a generic interface type as a return type in a factory, where the concrete type is a pointer and nil is returned as that pointer.
  • warningRefactoring a function that used to return a concrete pointer to return an interface without adjusting nil handling.
  • warningError handling that uses `errors.New` or `fmt.Errorf` correctly but later wraps the error in a custom type pointer that is nil.
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildAlways return a nil interface explicitly instead of a nil pointer: `if p == nil { return nil }` instead of `return p`.
  • buildUse concrete types in function signatures when possible; avoid returning interfaces unless necessary.
  • buildWhen returning an interface, use a helper that converts nil pointer to nil interface: `return ifaceNil[*MyError](p)` where ifaceNil is a generic function.
  • buildFor JSON unmarshaling, check if the field exists before unmarshaling into a pointer field, or use a wrapper type.
  • buildUse `reflect.ValueOf(iface).IsNil()` before using the interface value, but only after confirming the interface is non-nil.
  • buildIn unit tests, compare the underlying pointer using `reflect.DeepEqual` or by type-asserting to the concrete type and comparing to nil.
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedAdd a test that returns a nil pointer and asserts the interface is nil: `if err != nil { t.Fatal("expected nil") }` should pass after fix.
  • verifiedPrint the interface with `%#v` before and after fix to confirm the type is no longer set.
  • verifiedRun a stress test with edge cases: missing JSON fields, empty slices, etc., and ensure no nil pointer dereferences.
  • verifiedUse `go vet` and `staticcheck` to catch potential issues before they hit production.
  • verifiedMonitor error rates and logs: after fix, the symptom of 'silent nil' should disappear.
  • verifiedCover the fix with a regression test that explicitly tests the interface nil behavior.
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningDon't use `if err != nil` when the error variable is of type interface{} or a custom interface that can hold a nil pointer.
  • warningDon't assume that because the underlying pointer is nil, the interface is nil. They are different.
  • warningAvoid using named return values for interface types; it complicates nil handling.
  • warningDon't rely on `reflect.ValueOf(iface).IsNil()` without first checking that the interface is non-nil; it panics on nil interface.
  • warningDon't use `if err == nil` as a guard for calling methods on the error; if err is non-nil but holds nil pointer, it panics.
  • warningDon't ignore the warning from `go vet` about 'comparison of interface with nil' when the concrete type is a pointer.
( 07 )War story

Silent Failure in Payment Service Due to Interface Nil Pointer

Backend EngineerGo 1.20, PostgreSQL, gRPC, Docker, Kubernetes

Timeline

  1. 10:00Deploy new payment service version with custom error interface
  2. 10:15Alert: Payment processing rate drops 30%, no errors in logs
  3. 10:20Check logs: no error messages, but many responses have unexpected nil pointer
  4. 10:30Add debug logging: print error type and value before returning
  5. 10:35Deploy debug version, see: error interface holds *PaymentError(nil)
  6. 10:40Identify the bug: function returns a named *PaymentError that is nil
  7. 10:45Fix: return nil explicitly instead of the named variable
  8. 10:50Deploy fix, payment rate recovers to normal

We had a payment processing service written in Go that used a custom error interface. The function `ProcessPayment` returned an `error` interface, but internally it used a named return variable of type `*PaymentError`. The code looked like: `func ProcessPayment(ctx context.Context, req *PaymentRequest) (err error) { var pErr *PaymentError; ...; return pErr }`. When the payment succeeded, `pErr` was nil, but because the return type was `error`, the interface got the type `*PaymentError` and value nil. So `if err != nil` in the caller was true, and it tried to access `err.Error()` which panicked with nil pointer dereference because the underlying pointer was nil.

The symptom was a silent failure: the caller logged 'error' but the actual error message was never printed because `err.Error()` panicked. The panic was caught by a recovery middleware that logged a generic message, so we saw no actual error context. The payment was marked as failed even though it succeeded, causing a 30% drop in successful payments.

The fix was simple: change the function to return nil explicitly when there's no error: `if pErr == nil { return nil }`. This way the interface itself becomes nil. We also added a unit test that verifies the error interface is nil when the underlying pointer is nil. After deploying, the payment rate recovered. We also added a lint rule to catch returning named pointer variables from interface-returning functions.

Root cause

Returning a nil concrete pointer variable from a function that returns an interface causes the interface to be non-nil because the type information is set.

The fix

Explicitly return nil instead of the nil pointer variable. Also added a helper function that converts a nil pointer to a nil interface.

The lesson

Never return a named pointer variable from a function that returns an interface without checking for nil. Use explicit nil return or a wrapper.

( 08 )How Go Interfaces Work: The Two-Word Structure

In Go, an interface value is represented internally as a two-word structure: a pointer to the type information (itable) and a pointer to the data (value). When you assign a concrete value to an interface, both words are set. If the concrete value is a nil pointer of a type, the type pointer is non-nil (pointing to the type's metadata), but the data pointer is nil. This makes the interface itself non-nil because the type word is non-nil.

The critical insight: `iface == nil` checks only if both words are nil. So an interface with a nil pointer inside is NOT nil. This is why the comparison fails. Understanding this memory layout helps you predict when the gotcha occurs.

( 09 )Common Patterns That Trigger This Bug

Pattern 1: Returning a named error variable. Example: `func foo() (err error) { var e *MyError; ...; return e }`. Here `e` is nil, but `err` becomes non-nil interface.

Pattern 2: Using a generic interface return type in a factory. Example: `func NewSomething() Something { return (*something)(nil) }` where `Something` is an interface.

Pattern 3: JSON unmarshaling into a struct with interface fields. If the JSON field is omitted, the pointer field stays nil but the interface field (if any) might be set to a non-nil interface with nil pointer.

Pattern 4: Using `errors.As` or type assertions on an interface that holds a nil pointer can cause panics if not handled.

( 10 )How to Safely Check for Nil Interface with Underlying Nil Pointer

You can use reflection: `if iface != nil && reflect.ValueOf(iface).IsNil() { // interface is non-nil but holds nil pointer }`. But beware: if `iface` is nil, `reflect.ValueOf(iface)` panics. So always check `iface != nil` first.

Alternatively, you can use a type switch to extract the concrete type and compare to nil: `switch v := iface.(type) { case *MyType: if v == nil { ... } }`.

The safest approach is to avoid the situation altogether by ensuring you never assign a nil pointer to an interface. Return nil explicitly.

( 11 )Lint and Tooling to Catch This

Go vet can catch some cases: `go vet` flags interface comparisons with nil when the concrete type is a pointer? Actually, `go vet` does not catch this directly. But `staticcheck` has a check `SA4023` that warns about comparing interface with nil when the interface's concrete type is a pointer. Run `staticcheck -checks=SA4023 ./...`.

You can also use custom lint rules with `go-critic` or `revive`. For example, `revive` has a rule `unexported-return` that may help.

Code review: train your team to look for functions that return an interface but have a named return variable of a pointer type.

Frequently asked questions

Why does `err != nil` return true when the underlying pointer is nil?

Because `err` is an interface. An interface is nil only if both its type and value are nil. When you assign a nil pointer (e.g., `*MyError(nil)`) to an interface, the type is set to `*MyError`, making the interface non-nil even though the value is nil.

How can I check if an interface holds a nil pointer?

Use `reflect.ValueOf(iface).IsNil()`. But first ensure `iface` is not nil to avoid panic. Alternatively, use a type switch to extract the concrete type and compare to nil.

What is the best fix for this issue?

The best fix is to never return a nil pointer from a function that returns an interface. Instead, return nil explicitly. For example, change `return pErr` to `if pErr == nil { return nil } else { return pErr }`.

Does `errors.New` have this problem?

No, `errors.New` returns a non-nil interface with a concrete type `*errors.errorString` and a non-nil value. The problem occurs only when the concrete pointer itself is nil.

Can `go vet` detect this?

Standard `go vet` does not catch this. Use `staticcheck` with check `SA4023` or similar linters that analyze interface comparisons.