5 exercises — goroutines vs threads, channels and the Go concurrency model, defer/panic/recover, implicit interface satisfaction, and idiomatic error wrapping with %w.
A Go developer says: "We use goroutines for all the network calls." A colleague unfamiliar with Go asks what a goroutine is. What is the correct explanation?
A goroutine is Go's fundamental concurrency primitive. Launched with go func().
Key differences from OS threads: • Size: A goroutine starts at ~2 KB stack (grows dynamically); an OS thread typically needs 1–8 MB • Cost: You can run 100,000+ goroutines; OS threads are limited by the OS (typically thousands) • Scheduling: The Go runtime's scheduler (M:N scheduler) multiplexes goroutines onto OS threads. M goroutines → N OS threads.
Go concurrency vocabulary: • goroutine — lightweight concurrent execution unit (go f()) • channel — typed conduit for communicating between goroutines • M:N scheduler — many goroutines mapped onto fewer OS threads • GOMAXPROCS — number of OS threads used; defaults to CPU core count • goroutine leak — goroutines that never exit (common bug: goroutine blocked waiting on a channel with no sender)
2 / 5
A code review comment says: "You should use a channel here instead of a shared variable." In Go, what is a channel and why does it avoid the need for a mutex in this case?
Go channels are typed, concurrent-safe communication pipelines between goroutines.
Syntax: • Create: ch := make(chan int) (unbuffered) or make(chan int, 100) (buffered, capacity 100) • Send: ch <- value — blocks until receiver is ready (unbuffered) • Receive: value := <-ch — blocks until sender sends • Close: close(ch) — signals no more values will be sent
Unbuffered vs buffered: • Unbuffered: sender and receiver synchronise (rendez-vous). Send blocks until receive is ready. • Buffered: sender can proceed until buffer is full. Receiver blocks only when buffer is empty.
The Go proverb: "Do not communicate by sharing memory; instead, share memory by communicating." → Instead of a shared variable + mutex, pass ownership of data through a channel. One goroutine owns the data at a time.
Channel direction type hints: chan<- int (send-only), <-chan int (receive-only) — used in function signatures.
3 / 5
A Go function contains: defer file.Close() right after opening the file. An intern asks what defer does. What is the correct explanation?
defer is Go's built-in cleanup mechanism. It pushes a function call onto a stack that is executed in LIFO (last-in, first-out) order when the surrounding function returns.
Why it matters: Without defer, you must remember to close files/connections at every return point. With defer, you declare cleanup right after setup:
f, err := os.Open("data.csv")
if err != nil { return err }
defer f.Close() // guaranteed to run when function returns
// ... use f ...
Even if a panic occurs, deferred functions run (enabling rollback or logging).
LIFO order: if you have multiple defers, last declared runs first: defer a() defer b() → b() runs, then a()
Common uses: • defer file.Close() — close files • defer mu.Unlock() — release mutex • defer rows.Close() — close SQL result sets • defer cancel() — cancel context • defer recover() — recover from panic (must be inside a deferred function)
panic and recover: • panic(v) — stops normal execution, runs deferred functions, propagates up call stack • recover() — inside a deferred function only, halts the panic and returns the panic value
4 / 5
A Go team's code review uses the phrase "this type satisfies the interface implicitly." What makes Go's interface system different from Java or C#?
Go's interfaces are implicit (also called structural typing or duck typing). A type satisfies an interface simply by having the required methods — no explicit declaration is needed.
Example:
type Writer interface {
Write(p []byte) (n int, err error)
}
// Any type with a Write method satisfies io.Writer
// No "implements" keyword needed
Why this matters: • You can define an interface in your package and have types from other packages satisfy it — without modifying those packages • Enables powerful dependency injection and testing (pass a mock that satisfies the interface) • Keeps coupling minimal: code depends on behaviour (interface), not concrete type
Key Go interface vocabulary: • interface satisfaction — a type "satisfies" or "implements" an interface • empty interface — interface{} or any (Go 1.18+) — every type satisfies it • type assertion — v, ok := x.(ConcreteType) — check and extract concrete type from interface • type switch — switch v := x.(type) { case int: ... } • io.Reader, io.Writer — Go's most important standard library interfaces
5 / 5
Go code returns errors like: if err != nil { return fmt.Errorf("process order: %w", err) }. A developer from a Python background asks: "Why wrap the error with %w instead of just returning it?" What is the idiomatic Go answer?
Go's idiomatic error handling uses explicit if err != nil checks and error wrapping to build a chain of context.
Why wrap errors: Without wrapping: return err — caller sees "connection refused" with no context With wrapping: return fmt.Errorf("create order: load user: query database: %w", err) — caller sees exactly where in the call chain it failed
The error chain: Using %w (Go 1.13+) embeds the original error in the new one. This enables: • errors.Is(err, target) — checks if any error in the chain matches target • errors.As(err, &target) — extracts a specific error type from the chain
Example: if errors.Is(err, sql.ErrNoRows) { return ErrNotFound }
Go error handling vocabulary: • sentinel error — a predefined error value to check against (io.EOF, sql.ErrNoRows) • error wrapping — adding context to an error using fmt.Errorf("context: %w", err) • error chain — the chain of wrapped errors traversed by errors.Is()/errors.As() • custom error type — struct implementing the error interface (Error() string) • panic vs error — errors are expected failure cases; panics are truly exceptional (programming errors, nil pointer deref)