# Go Channel


Interview notes on Go channels. Covers: basic usage, unbuffered vs buffered channels, send/receive panic cases, timeouts and non-blocking operations, select semantics, the memory model, common patterns, leaks and deadlocks.

## Basic usage

Creating, sending, receiving, closing, ranging:

```go
ch := make(chan int)        // unbuffered
ch2 := make(chan string, 8) // buffered, capacity 8

ch <- 1        // send
v := <-ch      // receive
v, ok := <-ch  // ok=false means the channel is closed and the buffer is drained
close(ch)      // close; only the sender should do this

for v := range ch { // reads until closed, then exits automatically
}
```

Directional channels (`chan<- T` is send-only, `<-chan T` is receive-only). The direction is enforced at compile time; commonly used in function signatures to constrain usage:

```go
func produce(ch chan<- int) { ch <- 1 } // send-only
func consume(ch <-chan int) { <-ch }    // receive-only
```

## Unbuffered vs buffered

- Unbuffered (capacity 0): a send must wait for a receiver to be ready, and a receive must wait for a sender to be ready — the operation completes only when both sides arrive. This is why it is also called a synchronous channel. Two goroutines shake hands through it.
- Buffered: the sender does not block while the buffer has room; it blocks only when the buffer is full, waiting for a receiver to take an element. The receiver blocks when the buffer is empty. The buffer decouples the send and receive pace.

How the runtime decides (the `full()` function in src/runtime/chan.go): for an unbuffered channel it checks whether the receiver wait queue `recvq` is empty; for a buffered channel it checks whether `qcount` equals `dataqsiz`.

A few derived points:

- Buffer size does not affect correctness, only when blocking happens and throughput. Size is a hint, not a capacity guarantee.
- Data travelling through the buffer is "copied in, copied out"; when a waiter exists, the value is handed off directly (handoff) to the waiter without going through the buffer.
- Send and receive order is FIFO: the buffer is a ring queue with a write index `sendx` and a read index `recvx`; blocked senders/receivers are woken in queue order. Channel operations are protected by an internal lock, so concurrent sends and receives from multiple goroutines are safe without extra locking.
- Passing a value through a channel carries synchronization with it: the send happens-before the receive completes (see the memory model section), no extra lock needed.

## Send/receive panic cases

| Operation                       | Result                                                    |
| ------------------------------- | --------------------------------------------------------- |
| Send to a closed channel        | panic: send on closed channel                             |
| Close an already-closed channel | panic: close of closed channel                            |
| Close a nil channel             | panic: close of nil channel                               |
| Receive from a nil channel      | blocks forever (runtime parks the goroutine, not a panic) |
| Send to a nil channel           | blocks forever (same as above)                            |
| Receive from a closed channel   | returns zero value and ok=false immediately, no panic     |

Two points that are easy to mix up:

- Only "send" and "close" can panic; "receive" never panics. `close` is a broadcast: every waiting receiver (and any future receiver) gets the zero value, so receiving from a closed channel is a legal operation. This is also the basis of the done-channel broadcast for shutdown.
- Data left in the buffer after `close` can still be fully read; only after it is drained does the receive return the zero value, so `range` finishes cleanly.

A nil channel blocks sends and receives forever — completely different from a closed channel, don't confuse them: nil means blocking, closed means send panics / receive returns zero.

## Timeouts and non-blocking operations

Timeout on receive:

```go
select {
case v := <-ch:
    // got a value
case <-time.After(3 * time.Second):
    // timed out
}
```

For timeouts inside a loop, use `NewTimer` instead of `time.After`: `time.After` allocates a fresh Timer on every call, which is not collected until it fires, so repeated calls pile up; `NewTimer` with `defer timer.Stop()` is reusable:

```go
timer := time.NewTimer(3 * time.Second)
defer timer.Stop()
select {
case v := <-ch:
case <-timer.C:
}
```

Non-blocking send/receive (try-send / try-receive) with select + default:

```go
select {
case ch <- v:
    // send succeeded (buffer not full, or unbuffered with a waiting receiver)
default:
    // channel full or no receiver; do not block, take this path
}

select {
case v := <-ch:
default:
    // buffer empty; do not block
}
```

Extension: `context.WithTimeout` is also timer + channel under the hood — a combination of deadline and timeout — use it when multiple goroutines need to share the timeout signal.

## select semantics

select watches multiple channel operations and executes whichever is ready. A few hard rules:

- When multiple cases are ready at the same time, one is chosen with uniform pseudo-random selection, not in source order. The implementation shuffles the cases to guarantee fairness.
- A case whose channel is nil is disabled and never participates in the selection. A common trick: set a channel to nil to dynamically disable a case.
- If all cases block and there is no default, the goroutine parks and waits; if there is a default, it runs immediately — that is the non-blocking pattern.
- If all cases block and there is no default, the runtime detects that all goroutines are asleep and dies: fatal error: all goroutines are asleep - deadlock!.
- One more optimization: when a select statically has only 0 or 1 cases plus a default, the compiler rewrites it into a plain if check instead of the full select machinery.

## Memory model

### What is happens-before

Single-goroutine programs have no concurrency problem: code order is execution order, and a statement after `a = 1` always reads 1. With multiple goroutines this breaks down — goroutines run in parallel, the compiler reorders instructions, and the CPU has caches and out-of-order execution. A statement that "ran first" in one goroutine may not have "happened" yet from another goroutine's point of view: the write to `a` physically completed, but the new value is still sitting in a CPU cache and was never written back to memory, so the other goroutine reads the stale value.

So concurrent programs need a set of rules to answer one question: under what conditions is the effect of operation A guaranteed to be visible to operation B? That "guaranteed visible" relation is happens-before:

- If A happens-before B, then A, and every write before A, is guaranteed visible to B.
- It is a logical order, not physical time. B may physically finish first (out-of-order execution), but the model guarantees that "in effect, A happened first".
- It is transitive: A happens-before B and B happens-before C implies A happens-before C.

Where does happens-before come from:

- Inside a single goroutine: code order is happens-before (program order). Since `a = "hello"` is written before `c <- 0`, the write to a happens-before that send.
- Across goroutines: there is no happens-before relation by default. Two operations without a relation constitute a data race, and the result is undefined. Only synchronization primitives (channel send/receive, mutex lock/unlock, atomic operations) can establish the relation.

Think of it this way: each goroutine has its own timeline (program order), and synchronization primitives are the points that stitch those timelines together — a mutex lock sees every write before the corresponding unlock, a channel receive sees every write before the corresponding send. Where timelines are stitched, visibility is guaranteed; where they are not, you have a data race.

The channel rules (go.dev/ref/mem):

1. A send happens-before the completion of the corresponding receive. Everything the sender wrote before the send is guaranteed visible to the receiver once it gets the value. This holds for all channels, buffered or not — the guarantee example in the official docs uses a buffered channel of capacity 10.
2. Unbuffered channel: a receive happens-before the completion of the corresponding send. The reverse direction also holds, which is why an unbuffered channel is bidirectional synchronization. This rule only applies to unbuffered channels.
3. Closing a channel happens-before any receive that returns a zero value because the channel is closed.
4. For a channel with capacity C, the k-th receive happens-before the completion of the k+C-th send. This is rule 2 generalized to buffered channels: the receiver's effects only become visible to the sender after C more sends (the k+C-th send completes). With C=1, the first receive only relates to the second send, if it exists.

Rule 2 is counterintuitive, so let's explain it first. "Send completion" means the moment the sender confirms the data was taken and it can return — not the moment the data was handed off. An unbuffered send is not "fire and forget": the sender must wait until the receiver actually took the value. Using a package handoff as an analogy, the three events happen in this order:

Courier hands over the package (send) → the other side catches it (receive completes) → the courier lets go and leaves (send completes)

So the send happens-before the receive completes (rule 1), and the receive completes happens-before the send completes (rule 2). Together they make unbuffered send/receive a back-to-back synchronization point: the sender cannot pretend to finish (it must wait for the receiver to take the value), and the receiver cannot pretend to finish either (it must wait for the sender to provide the data). Rule 2 guarantees reverse visibility: data the receiver wrote before receiving is guaranteed visible to the sender after the send completes (go.dev/ref/mem has a swapped example: f writes a variable then does `<-c`, main does `c <- 0` then reads the variable — same guarantee).

Synchronization capability of buffered channels: the forward direction (sender → receiver) is guaranteed by rule 1 and holds for buffered channels too — putting data into the buffer is still a send, and the receiver reads exactly the data of that send from the buffer, so send and receive match. What's missing is only rule 2's reverse synchronization: send completion = data enters the buffer, without waiting for a receiver, so "receive before send completion" does not hold. Reverse visibility has to come from rule 4: the receiver's effects only become visible to the sender after C more sends.

### Classic question: why might the receiver's data be invisible if the channel is buffered

Watch the direction: with a sender writing and a receiver reading (the rule 1 direction), buffered channels are guaranteed — the guarantee example in the official docs uses `make(chan int, 10)`. What is NOT guaranteed with buffering is the reverse: the receiver writes data and the sender reads it. That relies on rule 2, which only applies to unbuffered channels.

```go
var a string
c := make(chan int) // unbuffered: guaranteed; change to make(chan int, 1): not guaranteed

go func() {
	a = "hello"
	<-c      // receive
}()
c <- 0      // send
print(a)    // guaranteed "hello" when unbuffered; could be "" when buffered
```

Why unbuffered is guaranteed: the send cannot complete until the receiver shows up. The write to a precedes the receive (program order), the receive happens-before the send completes (rule 2), and the send completing precedes the print (program order) — the chain is complete, so visibility is guaranteed.

Why buffered is not guaranteed: send completion = data enters the buffer, no waiting for the receiver. main may print right after dropping the data, when the child goroutine may not have run yet and may not have written a; even if it did write a, there is no synchronization edge between the write and the print — rule 2 does not apply (buffered channels don't have it), and rule 4 requires a "second send" to take effect, but there is only one send here. So `print(a)` is a data race, and "" or "hello" are both possible; the model does not guarantee anything.

### Variants that are guaranteed visible even with buffering

For the forward direction, buffering is completely fine — rule 1 holds for all channels, and the official guarantee example is a buffered channel of capacity 10:

```go
var a string
c := make(chan int, 10) // buffered is fine

go func() {
	a = "hello"
	c <- 0
}()
<-c      // reads exactly the data of this send; send and receive match
print(a) // guaranteed "hello"
```

For the reverse direction with buffering, rule 4 must kick in: with capacity C, the sender must send at least C more times after the receive — the k+C-th send completing is what synchronizes with the k-th receive. With capacity 1, send once more:

```go
var a string
c := make(chan int, 1)

go func() {
	a = "hello"
	<-c      // receive 1
}()
c <- 0      // send 1
c <- 1      // send 2: the 1+1-th send completes, synchronizing with receive 1
print(a)    // guaranteed "hello"
```

The chain: write to a (program order, before receive 1) → receive 1 → send 2 completes (rule 4) → print (program order). With only one send there is no chain — that is the exact mechanism behind "buffered is not guaranteed": rule 2 is missing, and rule 4 has no second send to hook onto. Rule 4 is also the theoretical basis for using a buffered channel as a semaphore: the buffer holds at most C in-flight elements, and the sender of the k+C-th send is guaranteed to see the effect of the k-th receive.

## Common patterns

Worker pool (producer-consumer): tasks go into `jobs`, a fixed number of workers consume them, and `close(jobs)` lets the workers' `range` exit naturally. Multiple workers writing to `results` concurrently is safe, but someone must read `results` — otherwise the buffer fills up and blocks the workers:

```go
jobs := make(chan int, 100)
results := make(chan int, 100)

for i := 0; i < 4; i++ {
	go func() {
		for j := range jobs { // exits automatically once jobs is closed and drained
			results <- j * 2
		}
	}()
}

for i := 0; i < 100; i++ {
	jobs <- i
}
close(jobs) // only the sender closes

for i := 0; i < 100; i++ {
	<-results
}
```

Fan-out / fan-in: one task channel distributes work to multiple workers (fan-out), and the workers' results merge into one channel (fan-in). On the fan-in side, multiple goroutines write the same channel without extra locking — this is the classic advantage of channels over shared memory plus locks.

Semaphore: a buffered channel of capacity N used as a semaphore to limit concurrency (rule 4 of the memory model is the theoretical basis):

```go
limit := make(chan struct{}, 3)
for _, w := range work {
	go func(w func()) {
		limit <- struct{}{} // take a slot; blocks when full
		w()
		<-limit             // release
	}(w)
}
```

Two goroutines alternating to print 1~100. An unbuffered channel acts as a token; whoever holds the token prints. The first value printed is guaranteed to be 1, not random: main's first step is sending the token, and an unbuffered send must wait for the receiver to be ready — if the child goroutine hasn't reached `<-ch` yet, main blocks on the send; after the child gets the token, its first action is printing 1, and only then does it send the token back to let main print 2. The order is locked in by the handshake. Note that the last step must not send the token again: the receiver has already exited, the send would block forever, and the runtime reports a deadlock:

```go
func main() {
	ch := make(chan struct{})
	go func() {
		for i := 1; i <= 100; i += 2 {
			<-ch
			fmt.Println(i)
			ch <- struct{}{}
		}
	}()
	ch <- struct{}{} // give the token first
	for i := 2; i <= 100; i += 2 {
		<-ch
		fmt.Println(i)
		if i < 100 { // no token on the last step, or nobody receives and it deadlocks
			ch <- struct{}{}
		}
	}
}
```

Graceful shutdown (done-channel broadcast): `close(done)` makes every goroutine waiting on done receive the zero value at the same time and exit — a one-to-many notification. Combine it with select to check the exit signal:

```go
done := make(chan struct{})
jobs := make(chan int)

go func() {
	for {
		select {
		case <-done:
			return // got the exit signal
		case j := <-jobs:
			// do work
		}
	}
}()

// shut everything down at some point
close(done)
```

Closing rules: only the sender closes, and a channel is closed exactly once. With multiple senders, do not let each one close: either introduce a dedicated coordinator goroutine that owns the close, or wrap `close` in `sync.Once`.

Channel vs mutex: use channels for transferring ownership and cooperative scheduling (task distribution, shutdown notification, pipelines); use mutexes for protecting shared data structures (counters, critical sections around a map). The official proverb: do not communicate by sharing memory; instead, share memory by communicating.

## Leaks and deadlocks

The typical goroutine leak: a send with no receiver, and no receiver will ever appear:

```go
ch := make(chan int)
go func() { ch <- 1 }() // no receiver; blocks forever; this goroutine leaks
```

How to track it down: `go vet`'s copylocks check does not catch this class of problem; the goroutine profile from pprof shows stacks stuck in `chansend`. In real projects, add timeouts/context as a fallback around sends and receives so a leak cannot hang the whole flow.

Deadlock: every goroutine blocked on channels — the runtime detects it and dies with fatal error: all goroutines are asleep - deadlock!. Both an unbuffered send/receive with no counterpart and circular waiting (A waits for B to send while B waits for A to send) trigger it.

## Interview Questions

### What is the difference between unbuffered and buffered channels?

Answer: An unbuffered channel requires sends and receives to pair. The sender waits for a receiver, the receiver waits for a sender, and both sides complete a synchronous handoff.

A buffered channel lets the sender place values into the buffer first. A send does not block while space remains, and a receive blocks when the buffer is empty. Buffering decouples their pace, but it does not remove the synchronization rules.

### What happens when you send to or receive from a closed channel?

Answer: Sending causes `panic: send on closed channel`. Receiving remains valid: buffered values are returned first, then receives immediately return the element type's zero value with `ok=false`.

```go
ch := make(chan int, 1)
ch <- 7
close(ch)

v1, ok1 := <-ch // 7, true
v2, ok2 := <-ch // 0, false
```

### What happens when you send to, receive from, or close a nil channel?

Answer: Sending to or receiving from a nil channel blocks forever. When a nil channel appears in a `select` case, that case can never become ready, so assigning nil is useful for dynamically disabling a branch.

Closing a nil channel causes `panic: close of nil channel`. A nil channel does not behave like a closed channel.

### How does select choose when multiple cases are ready?

Answer: Go makes a uniform pseudo-random choice among the cases that can proceed. It does not use source order and does not guarantee round-robin execution.

With a `default`, that branch runs only when no other case can proceed immediately. Without a `default`, the current goroutine parks until a case becomes ready.

### Who should close a channel, and how should multiple senders handle closing?

Answer: The party that can prove no more values will be sent should close the channel. This is usually the sole sender or a coordinator. A receiver should not close it because it cannot know whether another sender remains.

Multiple senders must not each attempt `close`. A coordinator goroutine can close the channel after every sender exits; when the only concern is duplicate closing, `sync.Once` can guard it.

### Which happens-before relationships does an unbuffered channel establish?

Answer: Happens-before means writes before one operation are guaranteed visible after the other. An unbuffered channel provides both rules:

- A send happens-before the completion of its corresponding receive.
- A receive happens-before the completion of its corresponding send.

The first rule also holds for buffered channels. The second is specific to unbuffered channels, which is why an unbuffered handoff provides synchronization in both directions.

### How can channels cause goroutine leaks, and how can they be prevented?

Answer: A goroutine leaks when it blocks on a send or receive and no matching operation can ever occur. Common cases include an unread result channel and an upstream pipeline stage that keeps sending after the downstream stage exits.

Give every blocking operation a cancellation path. A `select` can watch `context.Done()`, and channel ownership should make the closer explicit. For diagnosis, inspect the pprof goroutine profile for stacks stuck in `chansend` or `chanrecv`.

### When should you use a channel instead of a mutex?

Answer: Channels fit task distribution, pipelines, and shutdown notifications because the goal is to transfer data or ownership between goroutines.

A mutex fits counters, maps, and other shared state because the goal is to protect a critical section. Do not force simple shared state into a channel design merely to avoid locks.


---

> Author: Nite  
> URL: https://www.nite07.com/en/posts/go-channel/  

