Lead: twenty-two Go theory prompts beside live coding
If you are preparing for a Go backend interview, you already know that syntax questions are only the warm-up. The real signal comes from how well you understand why Go behaves the way it does under the hood: the scheduler, channels, garbage collector, slices, maps, interfaces, and runtime initialization.
This is a rewritten, interview-oriented cheat sheet: comparison tables, HTML diagrams, 30-second answers, and the pitfalls that fail a theory round. Read a block, study the table, then explain the diagram out loud in 60–90 seconds on a pair mock. Close a hole on express quizzes first if the names are rusty.
Disclaimer. These 22 questions sit beside live-coding in Go. Stack sizes (~2 KB) are interview-order-of-magnitude, not an audit of a specific runtime version. This is not a substitute for reading runtime, and the gym does not compile or grade your code or promise an offer.
1. What is a goroutine?
A goroutine is a lightweight unit of execution managed by the Go runtime, not by the operating system. You start one with go f(). The stack begins around 2 KB and grows or shrinks as needed. Scheduling is M:N: many goroutines run on fewer OS threads.
| Characteristic | Goroutine | OS thread |
|---|---|---|
| Managed by | Go runtime (G-M-P) | Kernel |
| Stack | ~2 KB, growable | ~1–8 MB, fixed |
| Creation cost | Cheap (user space) | Expensive (syscall) |
| Context switch | Cheap, in the runtime | Kernel, heavier |
| Typical count | Tens of thousands | Hundreds |
| Communication | Channels, first | Shared memory + locks |
Interview tip. Do not stop at “lightweight threads”. Say they are scheduled with G-M-P, stacks grow, and a blocking syscall parks the M while the P is reused. Practice that sentence on Find a partner.
2. How are channels built?
A channel is a typed conduit. The runtime struct is hchan: it is not “just a queue”. It holds a buffer (maybe empty), wait queues for blocked senders and receivers, and a lock that serializes send/receive/close.
| Field (order of magnitude) | Role on a whiteboard |
|---|---|
qcount / dataqsiz |
How many values sit in the buffer, and the buffer capacity |
buf / elemsize |
Backing storage and element width |
closed |
Close flag; receive after close returns the zero value |
sendx / recvx |
Ring-buffer indices |
recvq / sendq |
Sudogs: goroutines waiting to receive or send |
lock |
Mutex around the handshake — not a lock-free magic pipe |
-
Sender (G1)
Takes the lock. Copies into the buffer, or parks on
sendqif there is no space and no waiting receiver. -
hchan + lock
One mutex. Buffer if
dataqsiz > 0. Wait queues otherwise. Close is a state, not a sent value. -
Buffer (optional)
A successful send into a buffer does not mean the receiver ran. It means the value is in
hchan. -
Receiver (G2)
Takes the lock. Copies from the buffer, or parks on
recvq. Direct handoff if a sender is already waiting.
Interview tip. Memorizing every field is optional. Explaining lock + wait queues + optional buffer is not. Sketch this on a mock, not a playground iframe.
3. Buffered vs unbuffered channels
Unbuffered is a rendezvous: send and receive happen together. Buffered lets the sender proceed while space remains. That is rate smoothing, not “the other side already processed the value”.
| Topic | Unbuffered | Buffered |
|---|---|---|
| Creation | make(chan T) |
make(chan T, n) |
| Behavior | Rendezvous | Queue of size n |
| Send blocks when | No receiver is waiting | Buffer is full |
| Receive blocks when | No sender is waiting | Buffer is empty |
| Use case | Sync, ownership handoff | Worker pools, burst smoothing |
ch := make(chan int) // unbuffered
buf := make(chan int, 8) // send may proceed 8 times
Interview tip. Buffered is the wrong default when you need a handshake. Say that on a mock; a quiz only marks the hole.
4. What happens if you write to a nil channel?
A nil channel is declared and not initialized. Send and receive block forever. close panics. len and cap are 0. The useful trick: a nil case in select is disabled.
| Operation | Nil channel | Closed channel |
|---|---|---|
| Send | Blocks forever | Panics |
| Receive | Blocks forever | Zero value immediately |
close |
Panics | Panics (already closed) |
Pitfall. Nil is not closed. Closed receive returns zero now; nil receive never returns. Say that difference out loud on Find a partner.
5. How does the Go scheduler work?
G-M-P. Each P has a local run queue. There is a global queue. Work-stealing moves Gs from a busy P to an idle one. Since 1.14 the runtime can preempt asynchronously. P is not a CPU core. GOMAXPROCS is the number of Ps.
| Name | Role |
|---|---|
| G | Goroutine: stack, instruction pointer, status |
| M | OS thread that executes Go code when it holds a P |
| P | Logical processor: local queue, limiter for parallelism |
| Global queue | Overflow / fairness; not the hot path |
| sysmon | Background M: preemption, netpoll, retake |
-
Runnable G
Lands on the local P queue when possible, else the global queue.
-
Local P queue
The M that holds this P pops Gs and runs them. Idle P may steal from another P.
-
OS thread
Executes the G. A blocking syscall parks this M; the P can move to another M.
-
Core
Hardware. P count is
GOMAXPROCS, not “number of cores you named in the answer”.
Interview tip. If they ask “what is work-stealing?”, say an idle P takes Gs from another P’s local queue. Close a hole on scheduler vs GC on a deep quiz.
G-M-P dies if you only highlight it
Nil channel vs closed, P vs core, buffer vs rendezvous — say the constraint first. A partner hears the pause. A quiz only marks the hole.
6. What concurrency model does Go use?
CSP: Communicating Sequential Processes. The proverb is “do not communicate by sharing memory; share memory by communicating.” Mutexes are allowed. Channels come first when ownership moves.
| Topic | Shared memory | CSP / channels |
|---|---|---|
| Ownership | Everyone sees the same bytes; you lock | A value moves; the receiver owns it |
| Sync primitive | Mutex, RWMutex, atomic | Channel send/receive, select |
| When it wins | Caches, shared counters, maps with a lock | Pipelines, worker pools, cancellation |
7. How does the garbage collector work?
Concurrent tri-color mark-and-sweep, low pause, non-generational and non-compacting. Write barriers keep the color invariant while mutators run. GOGC defaults to 100. GOMEMLIMIT caps the heap.
-
Mark start (STW)
Short stop-the-world: enable write barriers, prepare roots.
-
Concurrent mark
Mutators keep running. Grey work is the mark queue. Write barrier records pointer writes.
-
Mark termination (STW)
Second short STW: drain remaining grey, disable barriers.
-
Concurrent sweep
Reclaim unmarked spans while the program runs. No compacting move of objects.
Correction. Do not claim the Go GC is generational by default. That answer is a common fail.
8. How are slices built?
A slice is a descriptor: pointer, length, capacity. append past capacity allocates a new backing array and copies. Growth is not always 2×. Passing a slice to a function copies the header; the pointer still names the same array.
Slice header
- pointer — first element of the backing array this view names
- len — how many elements the view exposes
- cap — how far you can grow without a new array
Pitfall. Mutating s[i] in a callee can change the caller’s view. append that reallocates will not. Slices are not “passed by reference”.
9. Slices vs arrays
[3]int is a value type whose size is part of the type. []int is a header. Arrays compare with ==. Slices do not — use slices.Equal or walk the elements.
| Topic | Array | Slice |
|---|---|---|
| Size | Fixed; in the type | Dynamic length |
| Assignment | Copies every element | Copies the header |
| Comparison | == |
Not with == |
| Use case | Fixed buffers, crypto blocks | Almost everything else |
10. How is a map built?
hmap points at buckets. A bucket holds up to eight key/value pairs, then an overflow chain. Growth evacuates incrementally. Iteration order is randomized. Lookup is amortized O(1). Keys must be comparable.
-
hmap
Count, flags, B (bucket shift), hash seed, pointer to buckets.
-
Bucket (≤8)
Tophash bytes, then keys and values packed in the bucket.
-
Overflow chain
Extra buckets when a slot set fills before a grow.
-
Evacuate
Grow copies buckets over time. You do not stop the world for a full rehash talk.
Interview tip. Maps are not thread-safe for writes. Concurrent reads with no writers are fine. Otherwise mutex or sync.Map. A race here belongs in a pet-project README with -race.
11. How are strings built?
A string is an immutable header: pointer + length. The bytes are usually UTF-8, but they can be arbitrary. len(s) is bytes, not runes — use utf8.RuneCountInString when asked about characters.
| Topic | string | []byte |
|---|---|---|
| Mutability | Immutable | Mutable |
| Header | pointer + len | pointer + len + cap |
| Use | Text you will not rewrite | Buffers, I/O, builders |
12. How are interfaces built?
iface is itab + data. eface (any) is _type + data. Implementation is implicit. Type assertion and type switch inspect the dynamic type.
-
Value
A concrete value, or a pointer to one, boxed into the interface.
-
itab
Interface type, concrete type, method table. Empty interface skips itab and stores
_type. -
data
Pointer to the concrete value (or a copy for small values, depending on size).
Classic pitfall. An interface holding a nil pointer is not nil: the type part is set. var p *T; var i error = p; i == nil is false.
13. var vs :=
:= is only inside functions. It can redeclare a name in a multi-assignment if at least one name is new (a, c := 3, 4). Package-level bindings use var.
| Topic | var |
:= |
|---|---|---|
| Where | Package or function | Function body only |
| Type | Optional if you assign | Inferred |
| Re-declaration | Same name in the same block: no | OK if one name is new |
| Zero value | Yes, without initializer | Must have a right-hand side |
14. Goroutine vs OS thread (again, shorter)
Same story as §1, plus the syscall follow-up: a blocking syscall may park the M. The P is handed to another M so other Gs keep running. That is why “goroutines are cheap” is not the same as “threads are cheap”.
15. How do channels work in practice?
Data, synchronization, fewer accidental races, worker pools. Operations: send, receive, close, the ok comma, range. Only the sender should close. Close with remaining senders panics. Close does not send a value — it is a state change on hchan.
v, ok := <-ch
for v := range ch { // ends after close and drain
}
16. Go interfaces vs Java interfaces
Go is implicit: a type implements Writer by having Write. Java names implements. Method sets differ for values vs pointers. any is not Java Object with a vtable of methods you call blindly — it is an empty interface. Composition over inheritance.
| Topic | Go | Java |
|---|---|---|
| Implements | Implicit (method set) | implements |
| Dispatch | itab method table | vtable / invokeinterface |
| Empty / top type | any / interface{} |
Object |
17. defer is LIFO, and it is function-scoped
Deferred calls run when the surrounding function returns, not when the block ends. Arguments are evaluated immediately. Use it to close files, unlock, and recover. Named results can still be assigned in a deferred func. The cost is small; do not drop defer for premature optimization.
Pitfall. A defer inside a loop still runs at function return. Unlock in a loop needs care — or unlock without defer when the critical section is the loop body.
18. String concatenation
Do not + in a loop. Use strings.Builder. bytes.Buffer is fine for mixed writes. strings.Join wins when you already have the parts.
| Approach | When |
|---|---|
+ |
Few pieces, not in a hot loop |
strings.Builder |
Growing a string in a loop |
bytes.Buffer |
Bytes and strings mixed |
strings.Join |
A slice of parts already exists |
19. Data races
Two goroutines, same memory location, at least one write, no happens-before. go run -race / go test -race. The detector reports races that occur in that run. It is not a proof of absence.
| Tool | Use |
|---|---|
| Channel | Ownership handoff, pipelines |
| Mutex / RWMutex | Shared maps, caches |
| atomic | Counters, flags |
| Immutable | Publish a snapshot, never mutate |
Practice. Put go test -race in a project README. Then say the race definition on a mock.
20. sync.Pool
Reuse short-lived objects. The pool may be cleared on any GC. Good for buffers. Bad for persistent state or TTL caches. Always reset what you Get. The pool is safe for concurrent Get/Put.
21. context
Deadlines, cancel, request-scoped values. Do not stuff optional parameters into WithValue — trace ID, auth, deadlines. Cancel walks down the tree.
| Constructor | Use |
|---|---|
| Background / TODO | Root; TODO is a smell in production |
| WithCancel | Manual cancel of children |
| WithTimeout / Deadline | RPC and request budgets |
| WithValue | Request-scoped keys only, not config |
-
Background
Root. Never canceled on its own.
-
WithCancel / Timeout
Child. Cancel or timer fires; children see Done.
-
WithValue
Decorates the tree. Does not cancel. Keep keys private types.
-
Goroutines
Select on
ctx.Done(). Do not leak workers after cancel.
22. Runtime linking and init()
init has no arguments and no return. You cannot call it. A package may have several. Use it sparingly; prefer explicit setup in main or a constructor.
-
Imported packages
Dependencies initialize first, depth-first, once.
-
Package vars
Then this package’s variable initializers.
-
init()
Then each
initin source order (implementation detail: do not depend on file order in an interview beyond “inits run”). -
main()
Only the main package. That is the program.
Interview answer templates (30 seconds)
| Prompt | Say this |
|---|---|
| Goroutine | Runtime G, ~2 KB stack, M:N on threads via P. Not a small pthread. |
| Channels | hchan: lock, optional buffer, sendq/recvq. Sync plus data. |
| Buffered vs not | Unbuffered is rendezvous. Buffer is capacity, not “processed”. |
| GC | Tri-color concurrent mark-sweep, two short STW, not generational. |
| Slices vs arrays | Array size is in the type. Slice is pointer/len/cap. Header is copied. |
| Go vs Java iface | Implicit method set vs implements. itab vs vtable. |
| Data race | Two Gs, one location, a write, no sync. -race finds occurrences. |
| context | Cancel and deadlines down the tree. WithValue is not a bag of options. |
Common pitfalls
| Wrong | Right |
|---|---|
| P is a CPU core | P is a logical processor; GOMAXPROCS sets how many |
| GC is generational | Tri-color concurrent, non-generational, non-compacting |
| Slices are passed by reference | Header is copied; backing array may be shared |
| Interface with nil pointer is nil | Type is set; i == nil is false |
| close sends a value | Close is a flag; receive gets zero + ok=false |
| Maps are thread-safe | Writes race; mutex or sync.Map |
| defer is block-scoped | Runs when the function returns |
Connect features to the runtime
Memorizing every hchan field is optional. Explaining why goroutines are cheap, how channels sync, G-M-P, GC pauses, representations, races, and context is not. Sorting and graphs live in the algorithms cheat sheet; Avito’s Go hiring loop is a separate article.
Ready to test it? Create an account, run an express quiz, then a human mock on Find a partner. Already have an account? Log in.
Questions people actually ask
Only one row stays open. These are follow-ups after a scheduler or channel prompt, not a glossary of what Go is.
-
Is a goroutine just a lightweight OS thread?
No. A goroutine is a runtime-scheduled unit (G) multiplexed onto OS threads (M) through processors (P). Stacks start around 2 KB and grow. Call it M:N scheduling, not a small pthread.
Link to this answer -
Is P a CPU core?
No. P is a logical processor. GOMAXPROCS sets how many Ps exist. A core runs an M that currently holds a P. Work-stealing moves Gs between local P queues, not between cores directly.
Link to this answer -
Is the Go GC generational?
No. The collector is concurrent tri-color mark-and-sweep: non-generational and non-compacting. Name write barriers, two short STW windows, GOGC, and GOMEMLIMIT. Do not claim generations by default.
Link to this answer -
Are slices passed by reference?
No. The slice header (pointer, len, cap) is copied. The pointer still names the same backing array, so element writes can leak to the caller. append may allocate a new array and then the caller will not see the extra length.
Link to this answer -
Why is an interface holding a nil pointer not nil?
The interface value stores a type in itab and a data pointer. If the data pointer is nil but the type is set, i == nil is false. Check the concrete pointer before boxing, or compare with a typed nil of the same dynamic type.
Link to this answer -
Are maps safe for concurrent writes?
No. Concurrent writes, or a write plus a read, are a data race. Use a mutex or sync.Map. Concurrent reads with no writers are fine. Iteration order is randomized on purpose.
Link to this answer -
Does Gym compile and grade my Go code?
No. Bring a tiny example from the express quiz, then match a human on Find a partner. You host the call. The gym does not compile Go, does not book a FAANG interviewer, and does not promise an offer.
Link to this answer -
When is a buffered channel the wrong answer?
When you need a rendezvous, or when you treat a successful send as proof the receiver already processed the value. A buffer only means the value sat in hchan. Unbuffered channels are the default handshake.
Link to this answer
Say G-M-P, then ship a worker pool
Open a search, pick one runtime prompt, and keep the quizzes for the holes. Already have an account? Log in and start matching.
Related: Algorithm and data-structure theory · Avito hiring loop (Go stack) · System Design theory · Pair mock interview · Last week before the interview