12. Edge Cases & Pitfalls

Avoid typed-nil errors, loop-capture traps, timer leaks, and unsafe misuse.

Question: Why is a typed-nil error a common pitfall, and how do you avoid it?

Answer: An interface value is a pair of (dynamic type, value). If you return a nil pointer of a concrete error type (e.g., *MyError(nil)), assigning it to error yields a non-nil interface because the dynamic type is set. err != nil then evaluates true. Always return a plain nil when there is no error.

Explanation: The trap occurs when functions declare (*MyError, error) or similar and return a typed nil. Prefer returning nil as the error and avoid returning typed-nil errors. Example:

var me *MyError = nil
// Bad: returning me as error makes a non-nil interface
return me

// Good:
return nil

Question: What are best practices for error wrapping and classification?

Answer: Wrap errors with %w to preserve causes, and use errors.Is/As to classify and branch on behavior.

Explanation: Wrapping adds context for observability while keeping programmatic checks reliable.

if err := repo.Save(ctx, u); err != nil {
    return fmt.Errorf("save user %s: %w", u.ID, err)
}

if errors.Is(err, os.ErrNotExist) { /* handle missing */ }
var se *SomeError
if errors.As(err, &se) { /* typed handling */ }

Question: Why should you avoid using time.After in a loop?

Answer: time.After creates a new time.Timer object each time it is called. If used in a loop that iterates frequently, it can allocate a large number of timers that are not garbage collected quickly, leading to a memory leak.

Explanation: The correct pattern is to create a single time.Timer outside the loop and reuse it with timer.Reset(). It's important to drain the timer's channel before resetting it to handle cases where the timer fired but the event was not yet received.

Question: When, if ever, is it appropriate to use the unsafe package in production code? What are the risks?

Answer: The unsafe package should be used extremely rarely, only in performance-critical code after profiling has proven it's a significant bottleneck, and only when you have a deep understanding of Go's memory layout. Common-but-risky use cases include unsafe.Pointer for type conversions without allocations (like []byte to string) or for low-level interoperability with C code via cgo.

Explanation: The unsafe package bypasses Go's type safety and memory safety guarantees. The risks are substantial:

  • Panics: Incorrect pointer arithmetic can lead to illegal memory access.

  • Undefined Behavior: Its behavior can change between Go versions. Code that works today might break tomorrow.

  • GC Issues: You can create pointers that the garbage collector doesn't understand, leading to memory leaks or premature collection.

Any use of unsafe must be heavily documented, isolated to the smallest possible scope, and accompanied by extensive tests to validate its assumptions.

Question: Why do goroutines in a loop all print the same value?

Answer: Capturing the loop variable by reference causes all goroutines to see the final value. Rebind inside the loop.

Explanation: for i := range xs { i := i; go func(i int){...}(i) } ensures each closure has its own copy.

for i := 0; i < 3; i++ {
    i := i
    go func() { fmt.Println(i) }()
}

Question: Why can defer in loops cause problems?

Answer: Defers run at function exit; deferring in a long loop can leak resources. Close per-iteration resources immediately or move the body to a helper function.

Explanation: Prefer explicit close once done with each item.

Question: What are common map pitfalls?

Answer: Writing to a nil map panics, iteration order is random, copying a struct with a mutex is unsafe, and concurrent writes without sync race.

Explanation: Initialize maps before use, do not rely on order, protect shared maps with locks or sharding.

Question: What about time handling pitfalls?

Answer: Use time.Since(start) (monotonic clock) for elapsed time; beware of wall-clock changes. Always store/emit UTC timestamps.

Explanation: time.Time carries monotonic component when constructed via time.Now() within the same process.