Go types & idioms
How append shares or copies a slice's array
A slice is three fields: a pointer to a backing array, a length, and a capacity. len is how many elements you can index; cap is how many the array can hold before it must grow. append writes into the existing array when the result fits inside cap, and allocates a new array (copying the old contents over) when it does not.
That one rule causes the classic bug:
a := []int{1, 2, 3}
b := a[:2] // len 2, cap 3, same array as a
b = append(b, 99) // fits in cap 3, so it writes a[2]
// a is now [1 2 99]
Wrong "append hands me a fresh slice, so b can't touch a." It only reallocates when cap runs out. Until then, every append into a shared array is visible through the other slice. This is also why you write s = append(s, x) and never bare append(s, x): when a reallocation does happen, append returns a new header and your old variable still points at the stale array. To take a private copy on purpose, use a[lo:hi:max] to cap sharing, or copy into a fresh slice.
What a map read and iteration guarantee
Reading a missing key never panics. You get the element type's zero value. Use the comma-ok form to tell absent from present-with-zero:
n, ok := m[key] // ok is false when the key is absent
A map handed to a function shares storage with the caller, so writes inside the function show up outside. Iteration order is randomized on every range loop, so any code that leans on order is a latent bug the runtime will surface eventually. You also cannot take the address of a map element (&m[k] does not compile), because a growth can move the entry.
Wrapping errors and the defer trap
Return errors as values and wrap with %w so callers can walk the chain. errors.Is(err, ErrNotFound) checks identity against a sentinel through every wrapped layer; errors.As(err, &target) pulls out a typed error so you can read its fields. Reach for %v over %w only when you want to hide the underlying error from callers.
defer evaluates its arguments when the defer statement runs, not when the surrounding function returns:
defer fmt.Println(time.Now()) // captures the time now, prints at return
Deferred calls run in LIFO order at function exit, which bites in loops. A defer f.Close() inside a for over many files keeps every file open until the whole function returns. Close in the loop body, or push the per-item work into a helper that returns each iteration.
Why a nil error is not nil
An interface value is a pair: a type and a value. It compares equal to nil only when both halves are empty. Return a concrete pointer that happens to be nil through an error result and the type half is already set, so the interface is not nil:
func find() error {
var e *NotFoundError // nil pointer
return e // interface holds (*NotFoundError, nil)
}
if find() != nil { /* this runs, even though the pointer was nil */ }
Wrong "I returned a nil pointer, so the caller sees nil." The caller sees a non-nil interface wrapping a nil pointer, and the if err != nil guard fires on a success path. Fix it by using the error type throughout and returning literal nil when there is no error, never a typed nil variable. Interviewers reach for this one constantly; naming the (type, value) pair and why the type half is non-empty is the answer they grade as depth.
Sentinel versus typed errors
Two styles, two different contracts. A sentinel is a package-level value matched by identity:
var ErrNotFound = errors.New("not found")
// caller: if errors.Is(err, ErrNotFound) { ... }
A typed error is a struct carrying fields, extracted with errors.As:
var perr *fs.PathError
if errors.As(err, &perr) { log.Print(perr.Path) }
Both Is and As walk the Unwrap chain, including the multi-error Unwrap() []error form, and an error can override matching with its own Is(error) bool method. Exporting a sentinel makes it part of your API: callers pin to it, so you cannot rename or drop it without a breaking change. Favor typed or opaque errors when the package needs room to evolve, sentinels for stable outcomes like io.EOF.
Method sets, receivers, and why a value fails to implement
A method with a pointer receiver lives in *T's method set, not T's. So a value of type T does not satisfy an interface that needs that method, and you get a compile error that reads like a lie:
type Stringer interface{ String() string }
func (c *Counter) String() string { return strconv.Itoa(c.n) }
var s Stringer = Counter{} // does not implement Stringer
var s Stringer = &Counter{} // ok
Value receivers are reachable through both T and *T, so for a type that carries mutable state, a pointer receiver is the safer default across the whole type.
How a slice can pin memory it no longer needs
Because a subslice shares the backing array, a small slice can keep a large array alive:
func firstLine(buf []byte) []byte { return buf[:80] } // pins all of buf
The collector frees the array only once no slice references it, so returning buf[:80] from a one-megabyte read holds the whole megabyte. copy the bytes you keep into a fresh slice and let the rest go. append's growth is amortized: it grows by a factor (close to doubling for small slices, tapering toward roughly 1.25x once large), so N appends cost O(N) total, and preallocating with make([]T, 0, n) when you know the count skips the intermediate copies.
beta
The interviewer part is in the works.
The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.