Go Developer interview prep
A spaced-repetition deck of 333+ Go Developer interview questions — organised by topic and difficulty, and resurfaced right before you'd forget. Preview a few cards below, then choose access to study the whole track on an Anki-style SM-2 schedule.
7 days free on monthly or yearly · every feature included.
What's covered
Every topic in this track, grouped the way you'd study it.
Language Basics: Slices, Maps & Strings
15 cardsInterfaces & Type System
6 cardsGoroutines, Channels & select
12 cardssync, atomic & context
12 cardsScheduler, GC & Memory
8 cardsErrors, defer & panic
7 cardsConcurrency Patterns (Live Coding)
8 cardsTesting, Tooling & Generics
7 cardsDistributed Systems
12 cardsKafka & Messaging
12 cardsCaching Deep-Dive
10 cardsPerformance & Concurrency
11 cardsReliability & Incidents
10 cardsDatabases
104 cardsDevOps & Infra
35 cardsSystem Design
29 cardsBehavioral
35 cardsSample questions
A few cards from the deck — reveal each answer, then choose access to study the full set on a schedule.
How does a slice differ from an array, and what does a slice look like under the hood?
How does a slice differ from an array, and what does a slice look like under the hood?
Short answer: An array is a fixed-size value type whose length is baked into the type itself ([3]int and [4]int are different types), and it is copied wholesale on assignment and when passed around. A slice is a lightweight descriptor over an array: a header of three fields {pointer to backing array, len, cap}. Only the header is copied; the data stays shared.
In depth:
- Array —
[N]T, length is part of the type, the data sits as one contiguous block right where it's declared (on the stack, in a struct, or on the heap) and is copied byte for byte. Pass it to a function and you get a copy of every element. - Slice —
[]T, three machine words:ptrto an element of the backing array,len(how much is visible) andcap(how much fits before a reallocation). - Slicing —
arr[1:3]copies no data; it builds a new header pointing into the same array.
s := arr[1:3]
slice header backing array [5]int
┌──────────┐ ┌───┬───┬───┬───┬───┐
│ ptr ────┼────────► │ 0 │ 1 │ 2 │ 3 │ 4 │
│ len = 2 │ ▲ ▲
│ cap = 4 │ └─ ptr └─ len
└──────────┘
⚠️ Common mistake: thinking s2 := s1 gives you an independent copy. Only the 24-byte header is copied — both slices point at the same backing array, and writing s2[0] = x is visible through s1.
How does Go achieve polymorphism without classes and inheritance?
How does Go achieve polymorphism without classes and inheritance?
Short answer: Polymorphism comes from implicit (structural) interface satisfaction: a type satisfies an interface automatically if it has the required method set — no implements. Instead of inheritance there is composition via embedding, and encapsulation is driven by the first letter's case.
In depth:
- Structural typing (duck typing) — "if it quacks like a duck". Type and interface are never explicitly linked; the link is checked by method signatures at compile time.
- Composition over inheritance — embedding promotes the embedded type's methods outward, but that is
has-a, notis-a. - Encapsulation — an uppercase name is exported, a lowercase one is private to the package.
type Stringer interface{ String() string }
type Point struct{ X, Y int }
func (p Point) String() string { // Point implements Stringer
return fmt.Sprintf("(%d,%d)", p.X, p.Y)
} // no "implements Stringer" — matching the method is enough
⚠️ Common mistake: calling embedding "inheritance". There are no subtypes and no virtual dispatch: the outer type does not convert to the embedded one, and an embedded method is not "overridden" and cannot see the outer type's fields.
How does a goroutine differ from an OS thread?
How does a goroutine differ from an OS thread?
Short answer: A goroutine is a lightweight unit of execution managed by the Go runtime, not the OS kernel. Its stack starts at ~2 KB and grows dynamically, while an OS thread reserves megabytes of fixed stack. The runtime multiplexes thousands of goroutines onto a small number of threads in an M:N model, so a single process comfortably hosts hundreds of thousands of goroutines.
In depth:
- Stack — goroutine: ~2 KB, grows/shrinks on demand; OS thread: a fixed 1–8 MB reserved up front.
- Scheduler — goroutines are scheduled by the Go runtime (the GMP model: G — goroutine, M — OS thread, P — processor context) in user space; threads are scheduled by the kernel.
- Switch cost — a goroutine switch never enters the kernel and is orders of magnitude cheaper than a thread context switch.
- Scale — threads realistically number in the thousands, goroutines in the hundreds of thousands and beyond.
| Criterion | Goroutine | OS thread |
|---|---|---|
| Stack | ~2 KB, grows | 1–8 MB, fixed |
| Scheduled by | Go runtime (M:N) | OS kernel |
| Switch | user space, cheap | syscall, expensive |
| Per process | hundreds of thousands | thousands |
⚠️ Common mistake: answering "it's a lightweight thread" and stopping there. Without mentioning the runtime scheduler and the M:N model the answer sounds junior — the interviewer wants the mechanics.
What's the difference between sync.Mutex and sync.RWMutex, and when is RWMutex actually worth it?
What's the difference between sync.Mutex and sync.RWMutex, and when is RWMutex actually worth it?
Short answer: Mutex gives exclusive access — one goroutine in the critical section at any moment. RWMutex splits the lock in two: under RLock() readers enter in parallel, while a writer's Lock() is exclusive and waits for all readers to leave. RWMutex only pays off where reads greatly outnumber writes.
In depth:
- Mutex — a plain lock:
Lock()/Unlock(), always a single owner. The default choice. - RWMutex —
RLock()for readers (many at once),Lock()for the writer (one, exclusive). - When to reach for RWMutex — only after profiling shows contention on reads and the read:write ratio is high (say 10:1 or more), and the critical section isn't microscopic.
| Criterion | sync.Mutex | sync.RWMutex |
|---|---|---|
| Readers | one at a time | in parallel |
| Writer | exclusive | exclusive |
| Overhead | lower | higher (more internal state) |
| When | by default | reads >> writes |
⚠️ Common mistake: using RWMutex "just in case" and warning about writer starvation. Go has none: RWMutex is write-preferring — a waiting Lock() blocks new readers (side effect: a recursive RLock() in one goroutine can deadlock). The real cost is the pricier acquire: for short sections a plain Mutex is often faster.
What does GOMAXPROCS control, and what's its default?
What does GOMAXPROCS control, and what's its default?
Short answer: GOMAXPROCS sets the number of P — the maximum count of OS threads executing Go code simultaneously. By default it equals the number of logical CPUs (runtime.NumCPU()) — the case since Go 1.5.
In depth:
- What it actually caps. Only parallel execution of Go code. Goroutines blocked on a syscall or network I/O don't count toward the limit — a process can have far more threads than
GOMAXPROCS. - How to change it. Via the
GOMAXPROCSenv var or at runtime withruntime.GOMAXPROCS(n). - The container trap. Historically the default took the whole node's core count and ignored the cgroup CPU limit: on a 64-core node limited to "2 CPU" the runtime spun up 64 P — extra context switches and throttling. The classic fix is
uber-go/automaxprocs. - Recent Go. As of Go 1.25 the runtime honors the cgroup CPU limit by default — on Linux the default rounds up from the limit, so check the version before reaching for the workaround.
// GOMAXPROCS=4 ./app — via the environment variable
runtime.GOMAXPROCS(4) // or at runtime
n := runtime.GOMAXPROCS(0) // 0 — read the current value without changing it
⚠️ Common mistake: on Go < 1.25, relying on the default inside Kubernetes with a CPU limit. Without automaxprocs the runtime sees all of the node's cores, not your allotted quota.
How do you wrap an error with context while keeping the chain for errors.Is/As?
How do you wrap an error with context while keeping the chain for errors.Is/As?
Short answer: Use fmt.Errorf with the %w verb: fmt.Errorf("read config: %w", err). It's %w that stitches the original error into the Unwrap chain, so errors.Is/errors.As keep seeing it. The verbs %v and %s insert only the text and break the chain.
In depth:
%wpreserves the chain — it wraps the error while keeping access to the original viaUnwrap. Add short context: what the code was doing, not a restatement of the error.%v/%sbreak the chain — use them only when you deliberately hide the underlying error from the caller (an abstraction boundary).- Layered context — each level adds its own prefix, giving a readable trail:
open user file: read config: permission denied.
if err != nil {
return fmt.Errorf("read config %q: %w", path, err)
}
// errors.Is(err, os.ErrPermission) still works
⚠️ Common mistake: handling an error twice — logging it and also returning it up. Each layer either wraps and returns, or (at the very top) logs — not both, or your logs double up.
Ready to make it stick?
Start your first session in under a minute. Your future self, mid-interview, will thank you.
Questions about this track
How should I prepare for a Go Developer interview?
Study the concepts you'll be asked to explain, not just the ones you can code. RecallDeck's Go Developer track gives you 333+ curated interview questions and resurfaces each one with an Anki-style SM-2 schedule right before you'd forget it — so the answers are still there under pressure on interview day.
What topics does the Go Developer track cover?
The Go Developer track is organised into the core areas Go Developer interviews actually test, grouped by topic and by difficulty (Concept, Junior, Middle, Senior). You can preview the full outline and sample questions above before signing in.
Is spaced repetition effective for Go Developer interview prep?
Yes. Actively recalling an answer and grading yourself honestly builds far more durable memory than re-reading notes. RecallDeck schedules each Go Developer card to reappear at the moment you're about to forget it, so your daily reviews shrink while your recall holds.
Can I try the Go Developer track before paying?
Yes. Monthly and yearly access include a seven-day trial of the complete Go Developer track, the full SM-2 scheduler, statistics, flexible pacing, and cram mode. You can cancel online before the first charge.