Skip to main content
β
Login Register

Go Interview Theory: 22 Core Questions Explained with Tables and Diagrams

Oil painting of two people at a table with a laptop, a graph of nodes and orange streams above them; IT Career Gym
Two silhouettes, one laptop, a graph of nodes. Name G-M-P, then say why a nil channel blocks.

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
  1. Sender (G1)

    Takes the lock. Copies into the buffer, or parks on sendq if there is no space and no waiting receiver.

  2. hchan + lock

    One mutex. Buffer if dataqsiz > 0. Wait queues otherwise. Close is a state, not a sent value.

  3. Buffer (optional)

    A successful send into a buffer does not mean the receiver ran. It means the value is in hchan.

  4. 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
  1. Runnable G

    Lands on the local P queue when possible, else the global queue.

  2. Local P queue

    The M that holds this P pops Gs and runs them. Idle P may steal from another P.

  3. OS thread

    Executes the G. A blocking syscall parks this M; the P can move to another M.

  4. 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.

Find a partner Express quiz Pet project

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.

  1. Mark start (STW)

    Short stop-the-world: enable write barriers, prepare roots.

  2. Concurrent mark

    Mutators keep running. Grey work is the mark queue. Write barrier records pointer writes.

  3. Mark termination (STW)

    Second short STW: drain remaining grey, disable barriers.

  4. 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.

  1. hmap

    Count, flags, B (bucket shift), hash seed, pointer to buckets.

  2. Bucket (≤8)

    Tophash bytes, then keys and values packed in the bucket.

  3. Overflow chain

    Extra buckets when a slot set fills before a grow.

  4. 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.

  1. Value

    A concrete value, or a pointer to one, boxed into the interface.

  2. itab

    Interface type, concrete type, method table. Empty interface skips itab and stores _type.

  3. 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
  1. Background

    Root. Never canceled on its own.

  2. WithCancel / Timeout

    Child. Cancel or timer fires; children see Done.

  3. WithValue

    Decorates the tree. Does not cancel. Keep keys private types.

  4. 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.

  1. Imported packages

    Dependencies initialize first, depth-first, once.

  2. Package vars

    Then this package’s variable initializers.

  3. init()

    Then each init in source order (implementation detail: do not depend on file order in an interview beyond “inits run”).

  4. 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

8 questions

Only one row stays open. These are follow-ups after a scheduler or channel prompt, not a glossary of what Go is.

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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.

Create an account → Find a partner Log in

← Back to the blog

Related: Algorithm and data-structure theory · Avito hiring loop (Go stack) · System Design theory · Pair mock interview · Last week before the interview

Newsletter

By subscribing you agree to our Privacy.