1. What happens when you send a value to an unbuffered channel when no goroutine is waiting to receive from it?
Answer: The sending goroutine blocks until another goroutine receives from the channel.
An unbuffered channel is the heart of Go’s “do not communicate by sharing memory; share memory by communicating” philosophy. It has no storage capacity — a send on an unbuffered channel can only complete when a matching receive is happening simultaneously.
So when you do ch <- value and no receiver is ready, the runtime parks the sending goroutine. It stays blocked until another goroutine executes <-ch, at which point the value is handed over directly and both goroutines continue. Nothing is cached, nothing is buffered, nothing is dropped.
This synchronous handoff is the key difference from a buffered channel, which has a queue of fixed capacity: a send fills a slot and returns immediately if there’s room, even with no receiver waiting.
The interview answer: the sender blocks until a receiver is ready. Unbuffered channels are synchronous.
Answer:
The sending goroutine blocks until another goroutine receives from the channel.
An unbuffered channel is the heart of Go’s “do not communicate by sharing memory; share memory by communicating” philosophy. It has no storage capacity — a send on an unbuffered channel can only complete when a matching receive is happening simultaneously.
So when you do ch <- value and no receiver is ready, the runtime parks the sending goroutine. It stays blocked until another goroutine executes <-ch, at which point the value is handed over directly and both goroutines continue. Nothing is cached, nothing is buffered, nothing is dropped.
This synchronous handoff is the key difference from a buffered channel, which has a queue of fixed capacity: a send fills a slot and returns immediately if there’s room, even with no receiver waiting.
The interview answer: the sender blocks until a receiver is ready. Unbuffered channels are synchronous.
2. What is the output of reading a closed channel in Go?
Answer: It immediately yields the zero value of the channel’s element type, and the comma-ok check returns false.
Reading from a closed channel never blocks and never panics. The channel has no more values to deliver, so the receive returns the zero value of the element type — 0 for ints, "" for strings, nil for pointers, and so on.
The two-value form is what you should use to detect closure:
val, ok := <-ch
ok is false when the channel is closed and drained; true otherwise. This is the standard way to loop safely:
for val := range ch { ... } // ranging handles closure automatically
Compare this with the send side: sending to a closed channel panics, but receiving from a closed channel is fine and returns the zero value. The asymmetry is intentional — receivers are the ones that need to keep draining gracefully.
The interview answer: reading a closed channel returns the zero value immediately with ok == false.
Answer:
It immediately yields the zero value of the channel’s element type, and the comma-ok check returns false.
Reading from a closed channel never blocks and never panics. The channel has no more values to deliver, so the receive returns the zero value of the element type — 0 for ints, "" for strings, nil for pointers, and so on.
The two-value form is what you should use to detect closure:
val, ok := <-ch
ok is false when the channel is closed and drained; true otherwise. This is the standard way to loop safely:
for val := range ch { ... } // ranging handles closure automatically
Compare this with the send side: sending to a closed channel panics, but receiving from a closed channel is fine and returns the zero value. The asymmetry is intentional — receivers are the ones that need to keep draining gracefully.
The interview answer: reading a closed channel returns the zero value immediately with ok == false.
3. What will happen if you send a value to a closed channel in Go?
Answer: It triggers an immediate runtime panic: panic: send on closed channel.
Closing a channel is a declaration: “no more values will be sent.” The runtime enforces that contract. If a goroutine tries to send after the channel is closed, the runtime panics — the panic propagates, and unless recovered, the program crashes.
This is the asymmetric counterpart of receiving (which is safe on a closed channel and returns the zero value). Sending is never safe on a closed channel, and there’s no way to “reopen” a channel.
The discipline that follows: only the sender should close a channel (it’s a send-side statement), a channel should be closed only once, and receivers should coordinate with ok checks or range rather than relying on sends stopping. When you see send on closed channel, it’s almost always a race between a sender finishing and another goroutine closing the channel too early.
The interview answer: a send on a closed channel panics immediately with panic: send on closed channel.
Answer:
It triggers an immediate runtime panic: panic: send on closed channel.
Closing a channel is a declaration: “no more values will be sent.” The runtime enforces that contract. If a goroutine tries to send after the channel is closed, the runtime panics — the panic propagates, and unless recovered, the program crashes.
This is the asymmetric counterpart of receiving (which is safe on a closed channel and returns the zero value). Sending is never safe on a closed channel, and there’s no way to “reopen” a channel.
The discipline that follows: only the sender should close a channel (it’s a send-side statement), a channel should be closed only once, and receivers should coordinate with ok checks or range rather than relying on sends stopping. When you see send on closed channel, it’s almost always a race between a sender finishing and another goroutine closing the channel too early.
The interview answer: a send on a closed channel panics immediately with panic: send on closed channel.
4. What is the output of the following channel selection snippet?
ch1 := make(chan string, 1)
ch2 := make(chan string, 1)
ch1 <- "one"
ch2 <- "two"
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
}
Answer: Randomly chooses between "one" and "two".
When multiple cases in a select are ready at the same time, Go does not prefer one. It picks pseudo-randomly among the ready cases — a deliberate design choice to keep scheduling fair.
This randomization exists to prevent starvation: if select always chose the first ready case, a hot channel could starve the others forever. By randomizing, Go guarantees that over time all ready channels get served.
If exactly one case is ready, it runs; if none is ready, select blocks (or, with a default, runs the default). Here both channels hold a value, so both cases are ready — and the answer is “either, chosen at random,” not a deterministic "one" or "two".
Answer:
Randomly chooses between "one" and "two".
When multiple cases in a select are ready at the same time, Go does not prefer one. It picks pseudo-randomly among the ready cases — a deliberate design choice to keep scheduling fair.
This randomization exists to prevent starvation: if select always chose the first ready case, a hot channel could starve the others forever. By randomizing, Go guarantees that over time all ready channels get served.
If exactly one case is ready, it runs; if none is ready, select blocks (or, with a default, runs the default). Here both channels hold a value, so both cases are ready — and the answer is “either, chosen at random,” not a deterministic "one" or "two".
5. What is a “deadlock” panic in Go?
Answer: The runtime detects that all goroutines are blocked — none can make progress — and crashes with fatal error: all goroutines are asleep - deadlock!
A deadlock happens when every goroutine is waiting on something that can never be satisfied: a channel send with no receiver, a receive with no sender, a mutex nobody will release, or circular waits between goroutines.
Go’s runtime actively watches for this. When it determines that the entire process is stuck — no goroutine runnable, all blocked — there’s no point continuing, so it raises the fatal error. Like concurrent map access, this is not recoverable via panic/recover; it terminates the program.
The classic trigger is a main goroutine blocked forever: sending on a channel nobody will receive from, with no other goroutines running. The runtime message tells you exactly what’s deadlocked.
The interview answer: a deadlock panic fires when all goroutines are asleep/blocked and no progress is possible — a fatal, non-recoverable runtime error.
Answer:
The runtime detects that all goroutines are blocked — none can make progress — and crashes with fatal error: all goroutines are asleep - deadlock!
A deadlock happens when every goroutine is waiting on something that can never be satisfied: a channel send with no receiver, a receive with no sender, a mutex nobody will release, or circular waits between goroutines.
Go’s runtime actively watches for this. When it determines that the entire process is stuck — no goroutine runnable, all blocked — there’s no point continuing, so it raises the fatal error. Like concurrent map access, this is not recoverable via panic/recover; it terminates the program.
The classic trigger is a main goroutine blocked forever: sending on a channel nobody will receive from, with no other goroutines running. The runtime message tells you exactly what’s deadlocked.
The interview answer: a deadlock panic fires when all goroutines are asleep/blocked and no progress is possible — a fatal, non-recoverable runtime error.
6. What is the difference between an unbuffered channel and a buffered channel with capacity 1?
Answer: An unbuffered channel requires a sender and receiver to be simultaneously ready — the send blocks until a receive happens. A buffered channel with capacity 1 lets one send complete without blocking, storing the value in the buffer even with no receiver waiting.
Unbuffered (make(chan T)):
- Zero storage. A send succeeds only when a matching receive is executing concurrently.
- Perfect synchronization: the value is handed off directly, and both sides must be present. The sender is guaranteed the receiver has taken the value.
Buffered capacity 1 (make(chan T, 1)):
- One slot of storage. A send to an empty buffer writes into the slot and returns immediately — no receiver needed. A second send, while the slot is full, blocks until a receive frees it.
- The buffer simply decouples senders from receivers by that one item’s worth of slack.
The interview answer: unbuffered = synchronous handoff requiring both sides ready; buffered(1) = one item can be sent non-blocking before a receiver arrives.
Answer:
An unbuffered channel requires a sender and receiver to be simultaneously ready — the send blocks until a receive happens. A buffered channel with capacity 1 lets one send complete without blocking, storing the value in the buffer even with no receiver waiting.
Unbuffered (make(chan T)):
- Zero storage. A send succeeds only when a matching receive is executing concurrently.
- Perfect synchronization: the value is handed off directly, and both sides must be present. The sender is guaranteed the receiver has taken the value.
Buffered capacity 1 (make(chan T, 1)):
- One slot of storage. A send to an empty buffer writes into the slot and returns immediately — no receiver needed. A second send, while the slot is full, blocks until a receive frees it.
- The buffer simply decouples senders from receivers by that one item’s worth of slack.
The interview answer: unbuffered = synchronous handoff requiring both sides ready; buffered(1) = one item can be sent non-blocking before a receiver arrives.
7. What is the result of applying the cap() function to a channel?
Answer: cap(ch) returns the channel’s buffer capacity — 0 for an unbuffered channel, and the declared buffer size for a buffered one.
cap works on slices, arrays, pointers to arrays, and channels. For a channel, it reports the capacity declared at creation:
make(chan int)— cap 0 (unbuffered).make(chan int, 5)— cap 5.
The complement is len(ch), which returns the number of elements currently queued in the buffer — how many sends are waiting for receives. So cap is the fixed size of the buffer; len is how full it is right now.
The interview answer: cap(ch) gives the channel’s buffer capacity; len(ch) gives the current number of buffered, unread elements.
Answer:
cap(ch) returns the channel’s buffer capacity — 0 for an unbuffered channel, and the declared buffer size for a buffered one.
cap works on slices, arrays, pointers to arrays, and channels. For a channel, it reports the capacity declared at creation:
make(chan int)— cap 0 (unbuffered).make(chan int, 5)— cap 5.
The complement is len(ch), which returns the number of elements currently queued in the buffer — how many sends are waiting for receives. So cap is the fixed size of the buffer; len is how full it is right now.
The interview answer: cap(ch) gives the channel’s buffer capacity; len(ch) gives the current number of buffered, unread elements.
8. What will happen if you attempt to close a nil channel?
Answer: A runtime panic: panic: close of nil channel.
close requires a real, initialized channel. A nil channel (the zero value, or an uninitialized declared channel) has no runtime object to close, so the runtime panics immediately.
This is one of the three classic channel misuse panics, alongside sending on a closed channel and closing an already-closed channel. The rules to remember:
close(nil)→ panic.close(ch)twice → panic on the second close.- send on a closed channel → panic.
The safe pattern is to structure code so exactly one goroutine (the sender) owns closing, once, when sending is done. The interview answer: close on a nil channel panics.
Answer:
A runtime panic: panic: close of nil channel.
close requires a real, initialized channel. A nil channel (the zero value, or an uninitialized declared channel) has no runtime object to close, so the runtime panics immediately.
This is one of the three classic channel misuse panics, alongside sending on a closed channel and closing an already-closed channel. The rules to remember:
close(nil)→ panic.close(ch)twice → panic on the second close.- send on a closed channel → panic.
The safe pattern is to structure code so exactly one goroutine (the sender) owns closing, once, when sending is done. The interview answer: close on a nil channel panics.
9. How does Go scheduler allocate OS threads to goroutines?
Answer: M:N scheduling — M goroutines are multiplexed onto N OS threads.
Go’s runtime scheduler is built on the M:P:G model:
- G — a goroutine (a lightweight stack + state).
- M — an OS thread (the actual execution unit the kernel knows).
- P — a logical processor (a context that holds a runnable goroutine queue; its count defaults to the number of logical CPUs, controlled by
GOMAXPROCS).
A goroutine doesn’t map 1:1 to a thread. The scheduler multiplexes many goroutines onto a small number of threads — an M picks up a G from a P’s queue, runs it until it blocks or yields, then picks up another G. When a goroutine blocks on I/O, the M parks and another M starts, so the CPU stays busy.
This M:N design is what makes goroutines cheap: you can spawn thousands or millions, and the runtime efficiently shares a handful of threads among them.
The interview answer: M:N scheduling — goroutines (G) multiplexed onto OS threads (M) via logical processors (P).
Answer:
M:N scheduling — M goroutines are multiplexed onto N OS threads.
Go’s runtime scheduler is built on the M:P:G model:
- G — a goroutine (a lightweight stack + state).
- M — an OS thread (the actual execution unit the kernel knows).
- P — a logical processor (a context that holds a runnable goroutine queue; its count defaults to the number of logical CPUs, controlled by
GOMAXPROCS).
A goroutine doesn’t map 1:1 to a thread. The scheduler multiplexes many goroutines onto a small number of threads — an M picks up a G from a P’s queue, runs it until it blocks or yields, then picks up another G. When a goroutine blocks on I/O, the M parks and another M starts, so the CPU stays busy.
This M:N design is what makes goroutines cheap: you can spawn thousands or millions, and the runtime efficiently shares a handful of threads among them.
The interview answer: M:N scheduling — goroutines (G) multiplexed onto OS threads (M) via logical processors (P).
10. What happens if you run select {} (an empty select statement) in a standalone Go program with no other active goroutines?
Answer: It blocks the current goroutine forever, and the runtime raises a fatal deadlock panic.
select with no cases has nothing to do and no default to escape to. It waits — indefinitely. With no other goroutines running (and even with some, if they also never make the empty select proceed), the program is stuck with all goroutines blocked.
The runtime detects this global standstill and panics: fatal error: all goroutines are asleep - deadlock!. Like all deadlock panics, it’s non-recoverable.
select {} is the canonical way to write “block forever” — used deliberately in some daemon-style main functions. If nothing else keeps the process alive, it’s the deadlock. The interview answer: blocks forever → fatal deadlock panic.
Answer:
It blocks the current goroutine forever, and the runtime raises a fatal deadlock panic.
select with no cases has nothing to do and no default to escape to. It waits — indefinitely. With no other goroutines running (and even with some, if they also never make the empty select proceed), the program is stuck with all goroutines blocked.
The runtime detects this global standstill and panics: fatal error: all goroutines are asleep - deadlock!. Like all deadlock panics, it’s non-recoverable.
select {} is the canonical way to write “block forever” — used deliberately in some daemon-style main functions. If nothing else keeps the process alive, it’s the deadlock. The interview answer: blocks forever → fatal deadlock panic.
11. How do you construct a read-only channel parameter in a function declaration?
Answer: func process(ch <-chan int) — the <-chan direction makes the parameter receive-only (read-only).
Channel directions are part of the type:
ch <-chan int— receive-only: the function can read from it but not send.ch chan<- int— send-only: the function can send to it but not receive.ch chan int— bidirectional.
The arrows point in the direction data flows — <-chan means “the channel produces values toward <-” (so you receive); chan<- means values flow into the channel (so you send).
Directional parameters enforce contracts at compile time: a function that only consumes items declares <-chan and physically cannot send, which documents intent and prevents misuse. The interview answer: <-chan int is a receive-only (read-only) channel parameter.
Answer:
func process(ch <-chan int) — the <-chan direction makes the parameter receive-only (read-only).
Channel directions are part of the type:
ch <-chan int— receive-only: the function can read from it but not send.ch chan<- int— send-only: the function can send to it but not receive.ch chan int— bidirectional.
The arrows point in the direction data flows — <-chan means “the channel produces values toward <-” (so you receive); chan<- means values flow into the channel (so you send).
Directional parameters enforce contracts at compile time: a function that only consumes items declares <-chan and physically cannot send, which documents intent and prevents misuse. The interview answer: <-chan int is a receive-only (read-only) channel parameter.
Premium Content
Unlock Channels & Goroutines and all premium lessons with a subscription.
From ₹199.99/year — See plans