Go Concurrency Interview Questions With Honest Answers
Seventeen Go concurrency interview questions with accurate answers, three puzzles run for real, and notes on what the interviewer is really checking.

Go concurrency questions in interviews are not trivia. They are a quick way to find out whether someone has written concurrent code that had to work. The questions are fairly stable, and the difference between a weak answer and a strong one is rarely knowledge of an obscure API. It is whether the answer states the rule, says why the rule exists, and knows where it stops applying. This page collects seventeen questions I would expect, with answers I would be satisfied to give, and notes on what the interviewer is usually listening for.
Tested with Go 1.25.6, macOS on Apple Silicon (arm64)
It follows the concurrency patterns hub and draws on goroutines, channels, mutexes and context. Three of the puzzles are run for real, so the outputs shown are what Go 1.25.6 printed.
Goroutines
What is a goroutine, and how is it different from a thread?
A goroutine is a function running concurrently, scheduled by the Go runtime onto a small pool of OS threads. It starts with a stack of about 2 KiB that grows and shrinks as needed, and creating one costs on the order of a microsecond, so programs can hold hundreds of thousands. An OS thread has a fixed, much larger stack reserved by the kernel and is scheduled by the kernel, which is why a program rarely runs more than a few thousand. The runtime multiplexes goroutines over threads and takes care of parking one that blocks, so the thread can run another (the scheduler).
What they listen for: that goroutines are scheduled in user space, that they are cheap and not free, and that you can still run out of them.
How many goroutines can you run?
There is no fixed limit. The practical limit is memory, a few kilobytes each, so a hundred thousand sleeping goroutines are a few hundred megabytes at worst. The real constraint is usually what they hold: connections, file descriptors, buffers. The honest answer includes "I would bound them", with a worker pool or a semaphore (bounded parallelism).
How does a goroutine stop?
It stops by returning. Go provides no way to kill a goroutine from outside, so cancellation is cooperative: the goroutine checks a context.Context or a done channel and returns. A goroutine with no such check and a blocking operation can never be stopped, which is a leak (stopping a goroutine).
Channels
What is the difference between buffered and unbuffered channels?
An unbuffered send completes only when a receiver takes the value, so it synchronizes the two goroutines. A buffered send completes as soon as there is room, and blocks only when the buffer is full. A buffer is a queue that absorbs bursts, not a way to make a slow consumer fast. Choose the size on purpose: a buffer of one is common, a large one hides backpressure (buffered and unbuffered).
What happens when you close a channel, and who should close it?
Receivers still get any buffered values, then zero values with ok false. Sending on a closed channel panics, and so does closing it twice or closing a nil channel. So the sender closes, because only the sender knows no more values are coming, and when there are several senders, one coordinator closes after they have all finished (closing channels).
Puzzle: what does this print?
package main
import "fmt"
func main() {
ch := make(chan int, 3)
ch <- 1
ch <- 2
close(ch)
for i := 0; i < 4; i++ {
v, ok := <-ch
fmt.Println(v, ok)
}
}1 true
2 true
0 false
0 falseThe two buffered values come out first with ok true, then the closed channel yields the zero value with ok false for as many receives as you like. People who have not seen it expect the program to block or panic on the third receive.
What is a nil channel good for?
A send or receive on a nil channel blocks forever, which sounds useless until you see it in a select: setting a channel variable to nil removes its case from consideration. It is the idiom for turning off one branch after its channel has closed (nil channels in select).
When does select choose a case?
If one case is ready it takes it. If several are ready it picks one uniformly at random, so case order is not priority. With a default case it never blocks. With no ready case and no default it blocks until one is ready (select fairness).
Synchronization
Channels or mutexes?
Use a mutex to protect state that several goroutines touch briefly. Use channels to pass ownership of data between goroutines, to signal events, and to coordinate stages of work. The proverb "share memory by communicating" is a design preference for ownership transfer, not a ban on mutexes, and the standard library uses plenty of both. A strong answer adds that you would measure before optimizing between them (channels or mutexes).
What is a data race, and how do you find one?
Two goroutines access the same memory at the same time, at least one writes, and nothing orders the accesses. Find them by running tests with go test -race, which reports the conflicting stacks. Fix them with a mutex, an atomic, a channel or by not sharing. The Go memory model defines what orders accesses (happens-before).
What does sync.WaitGroup do, and what is the common mistake?
It waits for a set of goroutines to finish: Add before starting each, Done when each finishes, Wait to block. The classic mistake is calling Add inside the goroutine, after Wait may have already returned. In Go 1.25, wg.Go(f) does both and removes the mistake (WaitGroup).
What is the difference between Mutex and RWMutex?
An RWMutex allows many readers at once or one writer. It helps when reads are frequent, the critical section is non-trivial and writers are rare. It costs more per operation than a Mutex, and a recursive read lock can deadlock if a writer is waiting in between. Default to Mutex and switch when a measurement says so (RWMutex).
Deadlocks and leaks
How do you get a deadlock, and how does Go report it?
When all goroutines are blocked and none can make progress. The runtime detects the simplest case, where every goroutine is asleep, and exits with fatal error: all goroutines are asleep - deadlock!. Puzzle:
package main
import "fmt"
func main() {
ch := make(chan int) // unbuffered, with no other goroutine to receive
ch <- 1
fmt.Println("unreachable")
}fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
example.test/lab/main.go:7 +0x34
exit status 2The send blocks forever because there is no receiver, so the runtime reports the deadlock and exits with status 2. What the runtime does not detect is a partial deadlock: if some other goroutine (a timer, a network poller) is still alive, the program just hangs. Two goroutines each waiting on the other's lock hang silently inside a server, which is why dumps and timeouts matter (deadlocks).
What is a goroutine leak?
A goroutine that blocks forever and is never collected, typically on a channel nobody will use again. There is no error, only memory growth. Find it with a goroutine dump or the goroutine count metric, and prevent it by giving every goroutine a way to stop (goroutine leaks).
Context and patterns
What is context for?
Carrying cancellation, deadlines and request-scoped values across API boundaries and goroutines. Pass it as the first parameter, never store it in a struct, and always defer cancel() for contexts you create. Cancellation is cooperative: a function has to check the context or pass it to something that does (context).
How would you limit concurrency?
A buffered channel as a semaphore, a fixed pool of workers reading from a jobs channel, or errgroup.SetLimit. The right choice depends on error handling needs and per-worker state, which is the substance of comparing the patterns.
What does this print, and why?
package main
import (
"fmt"
"sort"
"sync"
)
func main() {
var wg sync.WaitGroup
var mu sync.Mutex
var seen []int
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
seen = append(seen, i)
mu.Unlock()
}()
}
wg.Wait()
sort.Ints(seen)
fmt.Println(seen)
}[0 1 2]With go 1.22 or later in go.mod, each iteration has its own i, so the goroutines see 0, 1 and 2. The same file compiled as a module that declares go 1.21 behaves differently:
package main
import (
"fmt"
"sort"
"sync"
)
func main() {
var wg sync.WaitGroup
var mu sync.Mutex
var seen []int
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
seen = append(seen, i)
mu.Unlock()
}()
}
wg.Wait()
sort.Ints(seen)
fmt.Println(seen)
}[3 3 3]There the goroutines share one variable. The loop finishes before they run, so they printed [3 3 3], the final value of i three times. The answer interviewers want is the history: this changed in Go 1.22, and the language version in go.mod decides which semantics apply (loop variables).
How do you shut a service down gracefully?
Catch SIGTERM with signal.NotifyContext, stop accepting new work, let in-flight requests finish with Server.Shutdown and a deadline, stop background goroutines in reverse order of start, then close resources (graceful shutdown). Mention the platform's kill timeout: your drain has to fit inside it.
How to answer well
Three habits separate strong answers. State the rule in one sentence, then the reason. Give one example from code you have written or read, even a small one. Name the boundary: "channels are for ownership transfer, and for hot shared counters I would use an atomic". And do not claim certainty you lack. If the interviewer asks about scheduler internals you have not read, say what you know (work stealing, the G, M and P model) and where you would look.
If you are on the other side of the table, the questions that discriminate best are the ones with a trap in the code: a closed channel, a captured loop variable, a WaitGroup.Add in the wrong place. Ask the candidate to predict the output, then ask how they would find the bug in a real program. The official sources worth reading before an interview are the Go memory model, the Effective Go section on concurrency and the language specification on channels.


