Files
teamai-test/.teamai/skills/common/golang-safety/references/nil-safety.md
T

4.7 KiB

Nil Safety Deep Dive

Nil Pointer Receivers

MUST check for nil before calling methods on pointer receivers from external sources. A method call on a nil pointer does not always panic — it depends on whether the method dereferences the receiver:

type Logger struct {
    prefix string
}

// ✓ Safe on nil but NEVER do that — does not dereference l
func (l *Logger) IsEnabled() bool {
    return l != nil
}

// ✗ Panics on nil — dereferences l to access prefix
func (l *Logger) Log(msg string) {
    fmt.Printf("[%s] %s\n", l.prefix, msg)
}

var l *Logger
l.IsEnabled() // false — works fine
l.Log("test") // panic: nil pointer dereference

Anyway, NEVER call a method on a nil pointer.

Designing nil-safe receivers

When a nil receiver is a valid state (e.g., optional components), guard against it explicitly:

func (l *Logger) Log(msg string) {
    if l == nil {
        return // silently skip if no logger configured
    }
    fmt.Printf("[%s] %s\n", l.prefix, msg)
}

This pattern is useful for optional dependencies, but use it sparingly — a nil receiver usually signals a bug, not an intentional state. Document when nil is an expected value.

Nil Function Values

NEVER rely on nil function values — always validate before calling. Calling a nil func variable panics:

// ✗ Bad — panics if callback was never set
type Worker struct {
    onComplete func(result string)
}

func (w *Worker) Finish(result string) {
    w.onComplete(result) // panic if onComplete is nil
}

// ✓ Good — check before calling
func (w *Worker) Finish(result string) {
    if w.onComplete != nil {
        w.onComplete(result)
    }
}

Default function pattern

Provide a no-op default to avoid nil checks at every call site:

func NewWorker(opts ...Option) *Worker {
    w := &Worker{
        onComplete: func(string) {}, // no-op default
    }
    for _, opt := range opts {
        opt(w)
    }
    return w
}

Nil and Error Comparisons

Returning nil error correctly

Interface comparisons with nil MUST account for the nil interface trap. A function returning error must return the untyped nil, not a typed nil pointer:

// ✗ Bad — returns non-nil error interface
func validate(s string) error {
    var err *ValidationError // typed nil
    if s == "" {
        err = &ValidationError{Field: "name"}
    }
    return err // even when err is nil, interface is non-nil
}

// ✓ Good — return nil explicitly
func validate(s string) error {
    if s == "" {
        return &ValidationError{Field: "name"}
    }
    return nil
}

Checking error chains with nil

errors.Is(err, nil) returns true only if err is truly nil. It does not help with the nil interface trap — the trap occurs before the error reaches errors.Is.

Nil in Generic Code

The comparable constraint and nil

Generic code MUST handle the zero value of type parameters correctly. Type parameters constrained by comparable can be compared with ==, but nil is not always a valid value:

// ✗ Confusing — T may or may not be nillable
func IsZero[T comparable](v T) bool {
    var zero T
    return v == zero // works, but "zero" for *Foo is nil, for int is 0
}

// ✓ Better — be explicit about what "empty" means
func IsNil[T interface{ ~*U }, U any](v T) bool {
    return v == nil
}

Nil checks with unconstrained type parameters

You cannot compare an unconstrained type parameter to nil:

// ✗ Does not compile
func Check[T any](v T) bool {
    return v == nil // compile error: cannot compare T with nil
}

// ✓ Good — use reflect or constrain to pointer types
func IsNilPtr[T any](v *T) bool {
    return v == nil
}

Patterns for Nil-Safe APIs

Constructor with defaults

Require initialization through a constructor, making the zero value impossible for external callers:

type Client struct {
    httpClient *http.Client
    baseURL    string
}

// Constructor guarantees non-nil fields
func NewClient(baseURL string) *Client {
    return &Client{
        httpClient: http.DefaultClient,
        baseURL:    baseURL,
    }
}

Lazy initialization for zero-value usability

When you want the zero value to be usable but need internal resources:

type Cache struct {
    mu   sync.Mutex
    data map[string]any
}

func (c *Cache) Get(key string) (any, bool) {
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.data == nil {
        return nil, false
    }
    v, ok := c.data[key]
    return v, ok
}

func (c *Cache) Set(key string, val any) {
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.data == nil {
        c.data = make(map[string]any)
    }
    c.data[key] = val
}

→ See samber/cc-skills-golang@golang-error-handling skill for nil error comparison pitfalls.