Initial commit: Go SRE interview exercises
Includes solved 02-worker-pool and 09-safe-counter (mutex, atomic, sharded). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GbnJj9ZqEx9nUvzkjdwfMq
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
# 10 — Gotchas: "What Does This Print?"
|
||||
|
||||
The trivia round. Each subfolder is a tiny program. The drill:
|
||||
|
||||
1. Open `main.go`, **predict the exact output** (or panic/deadlock/compile error).
|
||||
2. `go run ./10-gotchas/exNN-.../` and check yourself.
|
||||
3. Read the answer below — then explain it OUT LOUD in one breath, because
|
||||
that's the actual interview skill.
|
||||
|
||||
Note: ex11 intentionally does not compile, and ex02 intentionally deadlocks —
|
||||
that IS the exercise. (This folder is excluded from `go build ./...` sanity by
|
||||
being all `main` packages; run them individually.)
|
||||
|
||||
---
|
||||
|
||||
## Answers (no peeking before predicting)
|
||||
|
||||
<details><summary><b>ex01 — loop variable capture</b></summary>
|
||||
|
||||
Go ≥1.22: `0 1 2` in some order — the `for` statement gives each iteration a
|
||||
fresh `i` (spec change in 1.22, one of the very few semantic changes ever made
|
||||
to Go). Pre-1.22: typically `3 3 3` — all closures captured the *same*
|
||||
variable, which was 3 by the time they ran. Old fixes: `go func(i int){...}(i)`
|
||||
or shadowing `i := i`. Always mention the version boundary; it proves your
|
||||
knowledge has a timestamp.
|
||||
</details>
|
||||
|
||||
<details><summary><b>ex02 — unbuffered send deadlock</b></summary>
|
||||
|
||||
`fatal error: all goroutines are asleep - deadlock!` An unbuffered send blocks
|
||||
until a receiver is ready; there is no other goroutine. Fix A:
|
||||
`make(chan int, 1)`. Fix B: do the send or receive in another goroutine.
|
||||
Key nuance: the runtime detects only *global* deadlock (every goroutine
|
||||
blocked). One stuck goroutine among many = silent leak, no error, ever.
|
||||
</details>
|
||||
|
||||
<details><summary><b>ex03 — closed channel semantics</b></summary>
|
||||
|
||||
Prints `1`, then `0 false`, then **panics**: `close of closed channel`.
|
||||
Buffered values survive close and drain normally; after drain, receive returns
|
||||
zero value + `ok=false` *immediately* (never blocks). Send on closed also
|
||||
panics. Recite the rules: only the sender closes; with multiple senders,
|
||||
nobody closes directly (coordinate via WaitGroup); closing is signaling, not
|
||||
cleanup — unclosed channels are GC'd fine.
|
||||
</details>
|
||||
|
||||
<details><summary><b>ex04 — nil channel in select</b></summary>
|
||||
|
||||
`b 42`. Operations on nil channels block forever, so a nil case is simply
|
||||
never ready — select ignores it. If *all* cases were nil (and no `default`):
|
||||
deadlock. This is a load-bearing idiom: set a channel var to nil to disable
|
||||
its case dynamically (see exercise 07's README for it powering a fan-in).
|
||||
</details>
|
||||
|
||||
<details><summary><b>ex05 — slice aliasing via append</b></summary>
|
||||
|
||||
`[1 2 99 4]` and `[1 2 3 4]`. First: `a[:2]` has len 2, **cap 4** — append
|
||||
fits within cap, writes into the shared backing array, clobbers `a[2]`.
|
||||
Second: full slice expression `a[:2:2]` caps the cap at 2 — append must
|
||||
reallocate, `a` untouched. Corollaries: this is why `append` returns a value;
|
||||
why functions receiving slices can visibly mutate the caller's data *sometimes*
|
||||
(only when no realloc happened — the worst kind of bug); and what
|
||||
`s[low:high:max]` is actually for.
|
||||
</details>
|
||||
|
||||
<details><summary><b>ex06 — defer + named returns</b></summary>
|
||||
|
||||
`6` then `5`; then `deferred: 2, 1, 0` at main's exit, LIFO. In `f`,
|
||||
`return 5` assigns to the *named* return `x`, then defers run and mutate it →
|
||||
6. In `g`, `return x` copies into an anonymous slot first → the defer's
|
||||
increment is invisible. Also hiding here: `defer fmt.Println("deferred:", i)`
|
||||
evaluates the *argument* `i` at defer time — so it's the deferred *closures*
|
||||
that capture, plain calls snapshot. Practical use of the f-pattern: wrapping
|
||||
errors on the way out via `defer func() { err = ... }()` with a named `err`.
|
||||
</details>
|
||||
|
||||
<details><summary><b>ex07 — typed nil interface</b></summary>
|
||||
|
||||
Prints `got error: boom` — the `err != nil` branch runs. An interface value is
|
||||
a (type, value) pair; here it's (`*MyErr`, nil) — a non-nil interface wrapping
|
||||
a nil pointer. Interface == nil only when BOTH halves are nil. (The method
|
||||
call then works because `Error()` never dereferences its nil receiver — nil
|
||||
receivers are legal in Go.) Fix: return literal `nil`, or declare
|
||||
`var err error` and assign. If you can whiteboard the two-word pair, you own
|
||||
this question.
|
||||
</details>
|
||||
|
||||
<details><summary><b>ex08 — concurrent map writes</b></summary>
|
||||
|
||||
Very likely dies with `fatal error: concurrent map writes` — and `recovered:`
|
||||
never prints, because it's a **runtime fatal error (throw), not a panic**:
|
||||
`recover()` cannot catch it, the process is gone. (Deliberate design: a
|
||||
corrupted map is unrecoverable state.) Fixes: mutex, `sync.Map` (append-mostly
|
||||
workloads only), or sharding. `-race` flags it deterministically even on runs
|
||||
that happen not to crash.
|
||||
</details>
|
||||
|
||||
<details><summary><b>ex09 — WaitGroup Add placement</b></summary>
|
||||
|
||||
Race: main can reach `wg.Wait()` while the counter is still 0 (no goroutine
|
||||
has run `Add` yet) → Wait returns instantly, prints `count: 0..3` — any value.
|
||||
Worse interleavings hit "WaitGroup misuse: Add called concurrently with Wait".
|
||||
Rule: `Add` in the *spawning* goroutine, before `go`. Plus `count++` is itself
|
||||
a data race (`-race` flags both). And always pass `*sync.WaitGroup` — copying
|
||||
one with nonzero state breaks the count (`go vet` catches the copy).
|
||||
</details>
|
||||
|
||||
<details><summary><b>ex10 — goroutine leak on timeout</b></summary>
|
||||
|
||||
Prints ~101 goroutines instead of 1. On timeout, nobody ever receives from the
|
||||
unbuffered `ch`; the worker goroutine blocks on `ch <- "body"` forever. At
|
||||
100 rps against a slow upstream that's OOM with a countdown. The
|
||||
one-character-ish fix: `make(chan string, 1)` — the send always succeeds, the
|
||||
orphaned result gets GC'd with the channel. The structural fix: pass ctx into
|
||||
the work and select the send against `ctx.Done()`. Detection in prod:
|
||||
`go_goroutines` metric trending up + pprof goroutine profile showing an army
|
||||
parked in `chan send` at one line.
|
||||
</details>
|
||||
|
||||
<details><summary><b>ex11 — method sets</b></summary>
|
||||
|
||||
Compile error: `T does not implement Stringer (method String has pointer
|
||||
receiver)`. Method set of `T` = value-receiver methods only; method set of
|
||||
`*T` = both. Fix: `var s Stringer = &T{}`. The *why*: interface values can
|
||||
hold unaddressable copies (map elements, temporaries), so the compiler can't
|
||||
conjure a `*T` from a `T` for you. Related asymmetry people forget: calling
|
||||
`t.String()` directly on an addressable `t` works fine — the compiler inserts
|
||||
`(&t)` — it's only interface *satisfaction* that's strict.
|
||||
</details>
|
||||
@@ -0,0 +1,20 @@
|
||||
// PREDICT the output before running. Then: go run . — and try with an older
|
||||
// toolchain mentally: what did this print before Go 1.22?
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 3; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
fmt.Println(i)
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// PREDICT: what happens? Then run it. Then fix it TWO different ways.
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
ch := make(chan int)
|
||||
ch <- 1
|
||||
fmt.Println(<-ch)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// PREDICT each printed line, and what the last statement does.
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
ch := make(chan int, 2)
|
||||
ch <- 1
|
||||
close(ch)
|
||||
fmt.Println(<-ch)
|
||||
v, ok := <-ch
|
||||
fmt.Println(v, ok)
|
||||
close(ch) // ???
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// PREDICT: which case fires? What if b were nil too?
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
var a chan int // nil
|
||||
b := make(chan int, 1)
|
||||
b <- 42
|
||||
select {
|
||||
case v := <-a:
|
||||
fmt.Println("a", v)
|
||||
case v := <-b:
|
||||
fmt.Println("b", v)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// PREDICT both printed slices. Explain the difference between the two appends.
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
a := []int{1, 2, 3, 4}
|
||||
|
||||
b := a[:2]
|
||||
b = append(b, 99)
|
||||
fmt.Println("after append to a[:2] ->", a)
|
||||
|
||||
a = []int{1, 2, 3, 4}
|
||||
c := a[:2:2]
|
||||
c = append(c, 100)
|
||||
fmt.Println("after append to a[:2:2] ->", a)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// PREDICT what f() and g() return, and the order of the deferred prints.
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func f() (x int) {
|
||||
defer func() { x++ }()
|
||||
return 5
|
||||
}
|
||||
|
||||
func g() int {
|
||||
x := 5
|
||||
defer func() { x++ }()
|
||||
return x
|
||||
}
|
||||
|
||||
func main() {
|
||||
fmt.Println(f())
|
||||
fmt.Println(g())
|
||||
for i := 0; i < 3; i++ {
|
||||
defer fmt.Println("deferred:", i) // when and in what order?
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// PREDICT: which branch runs? This is the most-asked Go question in existence.
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
type MyErr struct{}
|
||||
|
||||
func (*MyErr) Error() string { return "boom" }
|
||||
|
||||
func do() error {
|
||||
var e *MyErr // nil pointer
|
||||
return e
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := do(); err != nil {
|
||||
fmt.Println("got error:", err)
|
||||
} else {
|
||||
fmt.Println("no error")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// PREDICT: what happens? Can recover() save you? Run it a few times.
|
||||
// Then run with: go run -race .
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println("recovered:", r) // will this ever print?
|
||||
}
|
||||
}()
|
||||
m := make(map[int]int)
|
||||
for i := 0; i < 100; i++ {
|
||||
go func(i int) { m[i] = i }(i)
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
fmt.Println(len(m))
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// PREDICT: what are the possible outputs? What's the race here even if it
|
||||
// "usually works"? Run with -race a few times.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var wg sync.WaitGroup
|
||||
count := 0
|
||||
for i := 0; i < 3; i++ {
|
||||
go func() {
|
||||
wg.Add(1) // <-- what's wrong with Add here?
|
||||
defer wg.Done()
|
||||
count++
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
fmt.Println("count:", count)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// SPOT THE LEAK. Run it — it prints goroutine counts. Explain why the number
|
||||
// grows, then fix it with a one-character-ish change.
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
func fetch() (string, error) {
|
||||
ch := make(chan string)
|
||||
go func() {
|
||||
time.Sleep(50 * time.Millisecond) // pretend slow upstream
|
||||
ch <- "body"
|
||||
}()
|
||||
select {
|
||||
case body := <-ch:
|
||||
return body, nil
|
||||
case <-time.After(10 * time.Millisecond): // timeout fires first
|
||||
return "", errors.New("timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
for i := 0; i < 100; i++ {
|
||||
fetch()
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
fmt.Println("goroutines still alive:", runtime.NumGoroutine()) // expected: 1
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// PREDICT: does this compile? If not, why not, and what's the minimal fix?
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Stringer interface{ String() string }
|
||||
|
||||
type T struct{}
|
||||
|
||||
func (t *T) String() string { return "T" }
|
||||
|
||||
func main() {
|
||||
var s Stringer = T{}
|
||||
fmt.Println(s.String())
|
||||
}
|
||||
Reference in New Issue
Block a user