Go goroutines and channels interview questions, with answers
Concurrency is the reason many teams choose Go, so it is the topic Go interviews spend longest on. The building blocks are small: goroutines, started with the go keyword, and channels, which pass values between them and synchronise them at the same time. The questions test whether you can reason about what blocks, what happens when a channel is closed, how select chooses between channels, and how to share memory without data races.
The answers below cover goroutines, buffered and unbuffered channels, closing and ranging, select, races, deadlocks and stopping a goroutine, with code built and run on Go 1.27; each example is written so that its output is the same on every run. Then take the free Go diagnostic — ten questions across every Go topic in the bank — to see which of these you can explain but not yet predict.
The questions, with answers
1.What is a goroutine, and how is it different from an OS thread?
In short: A goroutine is a function running concurrently, scheduled by the Go runtime onto a few OS threads and starting with a stack of a few kilobytes, so thousands are cheap.
The go keyword starts a function call in a new goroutine and returns immediately. Goroutines are managed by the Go runtime, not the operating system: its scheduler multiplexes many goroutines onto a small number of OS threads, at most GOMAXPROCS running Go code at once, and switches between them when one blocks on a channel, a lock or I/O. A goroutine starts with a stack of a few kilobytes that grows as needed, where an OS thread reserves a large fixed stack, so a program can run hundreds of thousands of goroutines. The program exits when main returns, without waiting for other goroutines, so main has to wait explicitly, here by receiving from a channel.
done := make(chan string) go func() { done <- "from goroutine" }() fmt.Println(<-done) // from goroutine
2.What is the difference between a buffered and an unbuffered channel in Go?
In short: An unbuffered channel hands each value directly from sender to receiver, so both must be ready; a buffered channel holds up to its capacity, so sends block only when it is full.
make(chan int) creates an unbuffered channel. A send on it blocks until another goroutine receives, so the two goroutines meet at that point, which makes an unbuffered channel a synchronisation tool as well as a way to pass data. make(chan int, 2) creates a buffered channel with room for two values: sends succeed without a receiver until the buffer is full, and receives succeed without a sender while it holds values. len reports how many values are waiting and cap the buffer size, as below. A buffer smooths out bursts between a producer and a consumer, but it does not remove the need for someone to receive eventually.
ch := make(chan int, 2) ch <- 1 ch <- 2 fmt.Println(len(ch), cap(ch)) // 2 2 fmt.Println(<-ch) // 1
3.How does range over a channel work in Go, and who should close the channel?
In short: range receives values until the channel is closed and drained; the sender closes it to say no more values are coming, and receivers never close it.
for v := range ch keeps receiving until the channel has been closed and every value sent before the close has been received, then the loop ends. Without a close, the loop would wait forever once the sender stopped. Closing is therefore the sender's job: it is a signal that no more values will come, and only the side that sends knows when that is true. A receiver closing a channel risks a panic, since sending on a closed channel panics. A common shape is a generator function that starts a goroutine, returns a receive-only channel, <-chan int, and closes it with defer when the goroutine finishes, as gen does below.
func gen(n int) <-chan int { ch := make(chan int) go func() { defer close(ch) for i := range n { ch <- i * i } }() return ch } func main() { for v := range gen(4) { fmt.Printf("%d ", v) } fmt.Println() // 0 1 4 9 }
4.What does select do in Go?
In short: select waits on several channel operations at once and runs the first one that can proceed; a default case makes it non-blocking.
A select statement lists channel sends and receives as cases and blocks until one of them can proceed, then runs that case; if several are ready at once, it picks one at random, so no case starves. Adding a default case makes the select return immediately when nothing is ready. Combined with time.After, which returns a channel that delivers a value after a delay, select implements timeouts, as below: nothing is ever sent on ch, so the timer's case wins after 10 milliseconds. The same pattern with a done channel or a context's Done channel lets a goroutine stop waiting when asked to.
ch := make(chan int) d := 10 * time.Millisecond select { case v := <-ch: fmt.Println(v) case <-time.After(d): fmt.Println("timeout") } // timeout
5.What is a data race in Go, and how do you prevent one?
In short: A data race is two goroutines accessing the same variable at the same time with at least one writing; prevent it with channels, a mutex or atomic operations, and find it with -race.
When two goroutines touch the same memory concurrently and at least one writes, without synchronisation, the result is undefined: updates can be lost and values can be torn. The fixes are to give each piece of data a single owner and pass it over channels, to guard shared data with a sync.Mutex, or, for simple counters and flags, to use the sync/atomic types, as below, where a hundred goroutines increment an atomic.Int64 and none of the increments is lost. Go's race detector, enabled with go run -race or go test -race, reports races it observes at run time, and running tests with it is standard practice.
var n atomic.Int64 var wg sync.WaitGroup for range 100 { wg.Add(1) go func() { defer wg.Done() n.Add(1) }() } wg.Wait() fmt.Println(n.Load()) // 100
6.What is a deadlock in Go, and how does the runtime report it?
In short: A deadlock is when goroutines wait on each other so none can proceed; if every goroutine is blocked, the runtime stops the program with all goroutines are asleep - deadlock!.
A goroutine blocks on a channel operation that nothing will ever complete, such as a receive from a channel no one sends on, or a send on an unbuffered channel with no receiver. When every goroutine in the program is blocked like that, the runtime detects it and ends the program with fatal error: all goroutines are asleep - deadlock!, as the two-line example below does. The detector only fires when all goroutines are stuck; if some other goroutine is still running, such as a server loop, a blocked goroutine simply leaks. Timeouts with select and a clear owner for closing each channel prevent most cases.
ch := make(chan int) <-ch // deadlock
7.How do you stop a goroutine in Go?
In short: Go has no way to kill a goroutine from outside; you signal it, by closing a channel or cancelling a context, and the goroutine checks the signal and returns.
A goroutine ends only when its function returns, and there is no API to stop one from outside. So long-running goroutines are written to watch for a stop signal, usually in a select next to their real work. Closing a channel is a broadcast every receiver sees at once, which is why the worker below stops as soon as quit is closed. In production code the signal is usually a context.Context: cancelling it closes the channel returned by ctx.Done(), and the same context carries deadlines and flows through function calls, so one cancellation stops a whole tree of goroutines. A goroutine that never checks for a signal is a leak.
quit := make(chan struct{}) out := make(chan int) go func() { for i := 0; ; i++ { select { case out <- i: case <-quit: return } } }() fmt.Println(<-out, <-out) close(quit) // 0 1
How the diagnostic asks it
One question from the Go bank, exactly as a sitting would show it. The bank has 4 on goroutines & channels and 30 across Go.
What happens when this Go code runs?
ch := make(chan int) ch <- 1 fmt.Println(<-ch)
- 1It crashes: fatal error: all goroutines are asleep - deadlock!correct
- 2It prints 1
- 3It prints 0
- 4It does not compile: nothing receives from ch
An unbuffered channel has no storage: a send completes only when another goroutine is ready to receive at the same moment. Here main is the only goroutine, and it blocks on ch <- 1 before it ever reaches the receive on the next line. With every goroutine blocked, the Go runtime detects that nothing can make progress and stops the program with fatal error: all goroutines are asleep - deadlock!. It would print 1 with a buffered channel, make(chan int, 1), or with the send moved into its own goroutine. The compiler does not analyse channel usage, so it compiles.
Measure it
Reading answers tells you what’s true. A diagnostic tells you what you get wrong.
10 Go questions across its topics, easy to hard, about fifteen minutes. You get a readiness figure with the arithmetic shown, the topics you missed named, and a practice set sized for today. Free: 1 diagnostic a month and 15 problems a day. No card.