Read alongside
A Tour of Go: Methods and interfaces; Concurrency
Language fluency with runnable examples; do every exercise in the Methods and interfaces and Concurrency modules.
Objectives
- Explain the slice header (pointer, length, capacity) and predict when append writes into a shared backing array.
- Explain map semantics (nil maps, unspecified iteration order, no concurrent writes) and the nil-interface-holding-a-nil-pointer pitfall.
- Wrap errors with %w and inspect them with errors.Is and errors.As instead of comparing strings.
- Implement a bounded worker pool with channels, select and context cancellation that shuts down cleanly and leaks no goroutines.
- Find and fix a data race with the race detector, and explain the happens-before guarantees that channels and mutexes provide.
Why it matters
Go makes concurrency cheap to start and easy to get subtly wrong. In interviews conducted in Go, the recurring question is whether you can stop what you start: every goroutine needs an exit path, every request a deadline, every shared variable an owner or a lock. The other common bug class comes from value semantics: copying a slice copies a view, not the data.
Core ideas
- Slices are a (pointer, length, capacity) header over an array. Slicing shares the backing array, and
appendwrites in place when capacity allows and reallocates otherwise, so two slices cut from one array can overwrite each other's elements. Copy explicitly (slices.Clone,copy) or cap the capacity with a full slice expressions[lo:hi:hi]before handing out a sub-slice. - Maps refer to a runtime hash table. Reading a nil map returns zero values; writing to one panics. Iteration order is unspecified and varies between runs. Maps are unsafe when any goroutine writes concurrently; guard them with a mutex.
- Interfaces are satisfied implicitly by method sets. An interface value pairs a dynamic type with a value and is nil only when both are, so returning a nil
*MyErroras anerrorproduces a non-nil error. Keep interfaces small and define them where they are used. - Errors are ordinary return values. Add context with
fmt.Errorf("load config: %w", err)and inspect witherrors.Is(a sentinel) orerrors.As(a type), never by matching message strings. Reservepanicfor programming errors. - Goroutines and channels. A send on an unbuffered channel completes only when a receiver takes the value, synchronizing both sides; a buffered channel decouples them up to its capacity. Only the sender should close a channel. Receiving from a closed channel yields the zero value with
ok == false, sending on one panics, and a nil channel blocks forever, which is useful for disabling aselectcase. A goroutine blocked forever is a leak the runtime never reclaims. - select and context.
selectwaits on several channel operations and picks pseudo-randomly among the ready ones; adefaultcase makes it non-blocking. Pass acontext.Contextas the first parameter of request-scoped functions, watchctx.Done()in loops and selects, and always call the cancel function returned bycontext.WithCancelorcontext.WithTimeout. - Memory model and races. Channel operations, mutexes and
sync/atomiccreate happens-before edges. Without them, concurrent access where one side writes is a data race, and races on multiword values such as slices, strings, maps and interfaces can corrupt memory rather than merely return stale data.go test -racereports races that occur during the run.
Since Go 1.22, modules declaring Go 1.22 or later get a fresh loop variable per iteration, removing the classic bug of goroutines all capturing the final value.
Common misconceptions
- "Passing a slice copies its data." It copies the header; the callee can modify the caller's elements, and whether its appends are visible depends on capacity.
- "A nil pointer returned as an error is a nil error." The interface holds a non-nil type, so
err != nilis true. - "The race detector proves there are no races." It only finds races on code paths that execute during the run.
- "Channels must be closed to be freed." Unreachable channels are garbage-collected; close one only to tell receivers that no more values will come.
How to approach it
Work through A Tour of Go, then write a pipeline that stops cleanly when its context is cancelled and a worker pool with a fixed number of goroutines. Run both under -race, remove a lock on purpose and read the report. Then extend them with timeouts, error propagation and graceful shutdown.
Self-check before you finish
- When does append modify the array that another slice also sees?
- What happens when you write to a nil map? Read from one?
- How does errors.As differ from errors.Is?
- Who should close a channel, and what happens on a send after close?
- How do you stop a goroutine that is blocked receiving from a channel?
- What changed about loop variables in Go 1.22?