The chan keyword declares a channel type. A channel is a typed pipe that goroutines use to hand values to each other, and each send and receive also synchronizes the two goroutines involved. chan Job carries Job values in both directions, chan<- Job can only send and <-chan Job can only receive. A channel has to be created with make before use, because the zero value of a channel type is nil (Go spec).
TL;DR
make(chan Job)creates an unbuffered channel. A send blocks until another goroutine receives, so every send is a handoff.make(chan Job, 100)creates a buffered channel. Sends only block when the buffer is full, and receives only block when it is empty.chan<- Joband<-chan Jobin a function signature limit it to sending or receiving. The compiler rejects anything else.- The sender closes the channel, never the receiver.
for job := range jobsstops after a close, andv, ok := <-chreturnsok == falseonce the channel is closed and empty. - Sending on a closed channel panics, and so does closing a channel twice. Every operation on a
nilchannel exceptcloseblocks forever. fatal error: all goroutines are asleep - deadlock!means every goroutine is blocked. A leak is the quieter version, where only some goroutines are stuck. Go 1.27 finds those with thegoroutineleakprofile.selectwaits on several channel operations at once. It gives you timeouts, cancellation and non-blocking sends.
How do you declare and create a channel in Go?
Write chan followed by the element type. A variable declared that way is nil until you assign it a channel from make:
example.gogopackage main import "fmt" type Job struct { ID int URL string } func main() { var queue chan Job fmt.Println(queue == nil, len(queue), cap(queue)) queue = make(chan Job, 100) queue <- Job{ID: 1, URL: "https://api.example.com/webhooks/stripe"} queue <- Job{ID: 2, URL: "https://api.example.com/webhooks/github"} fmt.Println(len(queue), cap(queue)) job := <-queue fmt.Println(job.ID, job.URL) }
example.texttexttrue 0 0 2 100 1 https://api.example.com/webhooks/stripe
ch <- v sends and <-ch receives. The arrow always points in the direction the data moves. cap returns the buffer size you passed to make, and len returns how many values are sitting in the buffer right now. Values come out in the order they went in.
A channel is a reference to a runtime structure, like a map. Passing it to a function or storing it in a struct copies the reference, so every copy talks to the same channel. The Go var keyword and var vs make cover channels, maps and slices as the three types you normally create with make.
What is the difference between buffered and unbuffered channels?
An unbuffered channel has no storage. A send waits until a receiver takes the value, and a receive waits until a sender offers one. Both sides meet at the same moment. The Go spec puts it as "communication succeeds only when both a sender and receiver are ready".
Here an upload handler passes a file name to a virus scanner that takes 100ms to get going:
example.gogofunc main() { uploads := make(chan string) go func() { time.Sleep(100 * time.Millisecond) fmt.Println("scanner: ready") for name := range uploads { _ = name // virus-scan the file } }() start := time.Now() uploads <- "invoice-1001.pdf" fmt.Println("handler: send returned after", time.Since(start).Round(10*time.Millisecond)) }
example.texttextscanner: ready handler: send returned after 100ms
The send could not complete until the scanner was ready to receive. When the send returns, the handler knows the scanner has the value. That guarantee is useful on its own, and the Go memory model states it formally: a receive from an unbuffered channel is synchronized before the corresponding send completes.
A buffered channel stores up to cap values. With make(chan string, 10), the handler's send would have returned at once, and the scanner would have picked the file up 100ms later. The sender only blocks when all 10 slots are full.
What buffer size should a Go channel have?
Start with zero or one, and pick a bigger number only when you can say what it is for. A buffer doesn't make a slow consumer faster. If producers are steadily faster than consumers, a buffer of 1,000 fills up and then behaves like an unbuffered channel, with 1,000 values of extra latency in front of it.
Buffers help in three situations:
- A goroutine that produces one result.
make(chan Result, 1)lets it send and exit even if nobody receives, which is the fix for the leak shown further down. - Bursts. A metrics exporter that gets 50 samples at once and ships them in batches once a second can use a buffer the size of a normal burst.
- Limiting concurrency. A buffered channel with capacity N works as a semaphore that lets N goroutines run at once. The patterns section has an example.
What do chan<- and <-chan mean in Go?
They are directional channel types. chan<- Job is send-only and <-chan Job is receive-only. A bidirectional chan Job converts to either one automatically, so you create the channel once and hand each function only the direction it needs.
A webhook pipeline with a producer and a delivery stage looks like this:
example.gogotype Result struct { JobID int Status int } func produce(ids []int, out chan<- Job) { for _, id := range ids { out <- Job{ID: id, URL: fmt.Sprintf("https://hooks.example.com/%d", id)} } close(out) } func deliver(in <-chan Job, results chan<- Result) { for job := range in { results <- Result{JobID: job.ID, Status: 200} } close(results) } func main() { jobs := make(chan Job) results := make(chan Result) go produce([]int{101, 102, 103}, jobs) go deliver(jobs, results) for r := range results { fmt.Printf("job %d: %d\n", r.JobID, r.Status) } }
example.texttextjob 101: 200 job 102: 200 job 103: 200
The signatures document who owns which end. Reading deliver, you know it never writes to in and never closes it. The compiler enforces that. If deliver tries to send on its input or close it:
example.gogopackage main type Job struct{ ID int } func deliver(in <-chan Job) { in <- Job{ID: 1} close(in) }
example.texttext./main.go:6:2: invalid operation: cannot send to receive-only channel <-chan Job in (variable of type <-chan Job) ./main.go:7:8: invalid operation: cannot close receive-only channel in (variable of type <-chan Job)
Closing counts as a send-side operation, so only a chan<- or a bidirectional channel can be closed. That makes the usual ownership rule, "the sender closes", something the type system checks for you.
How do you close a channel in Go?
Call close(ch). Closing tells receivers that no more values are coming. It doesn't throw away values already in the buffer. Receivers get those first, and after that every receive returns immediately with the zero value:
example.gogofunc main() { events := make(chan string, 2) events <- "user.created" events <- "user.deleted" close(events) for i := 0; i < 3; i++ { ev, ok := <-events fmt.Printf("%q %v\n", ev, ok) } }
example.texttext"user.created" true "user.deleted" true "" false
The second value, ok, is true for a real value and false once the channel is closed and drained. A plain ev := <-events can't tell an empty string that was sent from the zero value of a closed channel, so use the two-value form whenever the channel might be closed.
for v := range ch does the same check for you. It receives until the channel is closed and empty, then leaves the loop. That is how deliver above knows when to stop, and why produce has to close jobs when it runs out of IDs. Without the close, deliver would wait forever for a fourth job.
Who should close a Go channel?
The goroutine that sends. The receiver can't know whether another value is on its way, and closing from the receiving side leads straight to the panics below. When several goroutines send on one channel, none of them can close it alone. A separate goroutine waits for all of them with a sync.WaitGroup and closes the channel afterwards. The worker pool below shows that shape.
Three mistakes panic at run time:
example.texttextpanic: send on closed channel panic: close of closed channel panic: close of nil channel
You don't have to close every channel. A channel nobody references is garbage collected whether it was closed or not. Close a channel when receivers need to know that the stream has ended, such as a range loop or a shutdown signal.
What happens when you send to a nil or closed channel?
Here is every combination in one place:
| Operation | nil channel | Open channel | Closed channel |
|---|---|---|---|
Send ch <- v | Blocks forever | Blocks until a receiver takes it or buffer space frees up | Panics |
Receive <-ch | Blocks forever | Returns a buffered value, or blocks until one arrives | Returns buffered values, then the zero value with ok == false |
close(ch) | Panics | Succeeds | Panics |
A select case on a nil channel is never chosen, which the merge example below relies on. A closed channel returns at once to every receiver, which makes close work as a broadcast. Every goroutine receiving from the channel wakes up at the same time.
Why does Go say "all goroutines are asleep - deadlock!"?
Because the runtime found that every goroutine in the program is blocked and none of them can ever wake up. The smallest version is a send on an unbuffered channel with no receiver:
example.gogofunc main() { uploads := make(chan string) uploads <- "invoice-1001.pdf" fmt.Println("queued") }
example.texttextfatal error: all goroutines are asleep - deadlock! goroutine 1 [chan send]: main.main() /app/main.go:7 +0x34
main is the only goroutine, and it waits on a send that nothing will receive. The trace names the blocked operation (chan send) and the line. The fixes are to receive in another goroutine, or to give the channel a buffer if a single value is all you need to park. Common Go mistakes lists this as the tenth mistake.
The detector only fires when every goroutine is stuck. A web server always has a goroutine waiting in the network poller for the next connection, and the detector doesn't count that as stuck, so a handler blocked on a channel never triggers it. The handler just stops, and its goroutine stays in memory until the process exits. This is called a goroutine leak.
How do you find goroutine leaks in Go?
A common leak looks like this. A checkout page asks three shipping carriers for a quote and takes the fastest answer:
example.gogofunc fastestQuote(ctx context.Context) (Quote, error) { ch := make(chan Quote) go func() { ch <- fetchQuote("ups", 50*time.Millisecond) }() go func() { ch <- fetchQuote("dhl", 80*time.Millisecond) }() go func() { ch <- fetchQuote("fedex", 300*time.Millisecond) }() select { case q := <-ch: return q, nil case <-ctx.Done(): return Quote{}, ctx.Err() } }
The function receives once and returns. The two slower goroutines then try to send on an unbuffered channel that nobody will ever read. They stay blocked for as long as the process runs. After 100 checkouts, 200 goroutines are stuck.
Go 1.27 made the goroutineleak profile generally available, after one release as an experiment in Go 1.26 (Go 1.27 release notes). The garbage collector looks for goroutines blocked on a channel or lock that is no longer reachable from any runnable goroutine, and reports them by stack:
example.gogopprof.Lookup("goroutineleak").WriteTo(os.Stdout, 1)
example.texttextgoroutineleak profile: total 200 100 @ 0x1044f99a8 0x104492854 0x104492468 0x104550560 0x1044ffc74 # 0x10455055f main.fastestQuote.func3+0x4f /app/main.go:27 100 @ 0x1044f99a8 0x104492854 0x104492468 0x1045505d0 0x1044ffc74 # 0x1045505cf main.fastestQuote.func2+0x4f /app/main.go:26
It points at the DHL and FedEx goroutines by line. A server that imports net/http/pprof exposes the same data at /debug/pprof/goroutineleak. What's new in Go 1.27 covers the profile in more detail.
The fix is a buffer with room for every sender:
example.gogoch := make(chan Quote, 3)
Now every goroutine can send and exit, whether or not anyone reads its value. With that change, the same program reports goroutineleak profile: total 0. More generally, every goroutine you start needs some way to finish. Usually that is a receiver that always reads, a buffer big enough for every send, or a select on ctx.Done().
How does select work with channels in Go?
select waits on several channel operations and runs the first one that can proceed. If several are ready at once, it picks one at random, so no case can starve the others (Go spec).
How do you add a timeout to a channel receive?
Put the receive and a timeout channel in the same select. With a context.Context, the timeout channel is ctx.Done():
example.gogofunc lookupPrice(ctx context.Context, sku string) (int, error) { result := make(chan int, 1) go func() { result <- slowPriceService(sku) }() select { case cents := <-result: return cents, nil case <-ctx.Done(): return 0, fmt.Errorf("price for %s: %w", sku, ctx.Err()) } } func main() { ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() fmt.Println(lookupPrice(ctx, "SKU-1001")) fmt.Println(lookupPrice(context.Background(), "SKU-1001")) }
example.texttext0 price for SKU-1001: context deadline exceeded 4999 <nil>
slowPriceService takes 300ms, so the first call gives up after 100ms and the second one waits for the answer. The buffer of 1 on result is the leak fix from the previous section. After a timeout, the goroutine can still send its late answer and exit.
case <-time.After(2 * time.Second): works too when there is no context. Since Go 1.23, a timer nobody references can be garbage collected before it fires, so time.After inside a loop no longer piles up timers (Go 1.23 release notes). Go 1.27 removed the asynctimerchan GODEBUG setting that brought back the old behavior. In request-handling code, prefer the context, because it also carries cancellation from the client.
How do you do a non-blocking send or receive?
Add a default case. select runs default when no other case is ready, so the operation never waits. A metrics exporter that must never slow down the request path uses this to drop samples when its queue is full:
example.gogotype Exporter struct { queue chan Metric dropped atomic.Int64 } func (e *Exporter) Record(m Metric) { select { case e.queue <- m: default: e.dropped.Add(1) } } func main() { e := &Exporter{queue: make(chan Metric, 3)} for i := range 5 { e.Record(Metric{Name: "http_requests_total", Value: float64(i)}) } fmt.Println("queued:", len(e.queue), "dropped:", e.dropped.Load()) }
example.texttextqueued: 3 dropped: 2
Record runs in many request handlers at once, so the drop counter is an atomic.Int64 rather than a plain int. Keep a count of the drops, since a silent default hides the moment the consumer falls behind.
Why set a channel to nil in a select?
To switch a case off. A receive from a nil channel blocks forever, so a select case on a nil channel can never be chosen. When you merge two streams, set each one to nil once it's closed, and stop when both are:
example.gogofunc merge(a, b <-chan string) []string { var out []string for a != nil || b != nil { select { case v, ok := <-a: if !ok { a = nil continue } out = append(out, v) case v, ok := <-b: if !ok { b = nil continue } out = append(out, v) } } return out }
Without the nil assignment, a closed channel is always ready, and the loop would spin on zero values at full CPU.
What are the common channel patterns in Go?
Four patterns cover most channel code in production services.
Worker pool
A fixed number of goroutines read from one job channel. This one delivers webhooks with three workers:
example.gogofunc main() { hooks := make(chan Webhook) deliveries := make(chan Delivery) var wg sync.WaitGroup for worker := range 3 { wg.Go(func() { for h := range hooks { deliveries <- Delivery{WebhookID: h.ID, Worker: worker, Status: send(h)} } }) } go func() { for id := 1; id <= 9; id++ { hooks <- Webhook{ID: id, URL: fmt.Sprintf("https://partner.example.com/hooks/%d", id)} } close(hooks) }() go func() { wg.Wait() close(deliveries) }() ok := 0 for d := range deliveries { if d.Status == 200 { ok++ } } fmt.Println("delivered:", ok) }
example.texttextdelivered: 9
Four details make it correct. The producer closes hooks, so each worker's range loop ends. wg.Go, added in Go 1.25, starts each worker and tracks it in one call. A separate goroutine waits for all workers before closing deliveries, because no single worker knows when the others are done. The closures capture worker safely since Go 1.22 gave each loop iteration its own variable (see how closures work).
Fan-out and fan-in
The worker pool is fan-out: one channel feeds many goroutines. The deliveries channel is fan-in: many goroutines write to one channel that a single loop reads. The merge function above is fan-in for a fixed number of input channels. The Go blog's pipelines article builds longer chains out of the same two pieces.
Semaphore
A buffered channel of empty structs limits how many goroutines do something at once. An image service that must not resize more than three images in parallel:
example.gogosem := make(chan struct{}, 3) var wg sync.WaitGroup for _, img := range images { wg.Go(func() { sem <- struct{}{} defer func() { <-sem }() resizeImage(img) }) } wg.Wait()
The fourth goroutine blocks on sem <- struct{}{} until one of the first three releases its slot. The memory model guarantees this for a channel of capacity C: the kth receive happens before the (k+C)th send completes (Go memory model). When the goroutines also return errors, errgroup.Group with SetLimit from golang.org/x/sync does the same job and collects the first error.
Done signal with chan struct{}
A channel that is only closed, never sent on, broadcasts a signal to every receiver. struct{} is the element type because it takes zero bytes and makes clear that no data travels (see the Go struct keyword):
example.gogofunc flushLoop(done <-chan struct{}, flushed chan<- int) { ticker := time.NewTicker(50 * time.Millisecond) defer ticker.Stop() n := 0 for { select { case <-ticker.C: n++ case <-done: flushed <- n return } } } func main() { done := make(chan struct{}) flushed := make(chan int) go flushLoop(done, flushed) time.Sleep(175 * time.Millisecond) close(done) fmt.Println("flushes before shutdown:", <-flushed) }
example.texttextflushes before shutdown: 3
context.Context is built on the same idea. ctx.Done() returns a <-chan struct{} that gets closed on cancellation, so it works in any select the same way done does above.
Should you use a channel or a mutex in Go?
Use a channel when you are passing ownership of data or coordinating goroutines. Use a sync.Mutex when several goroutines update one piece of state in place. The Go proverb "Don't communicate by sharing memory, share memory by communicating" (Go Proverbs) describes the first case, and the Go wiki page Use a sync.Mutex or a channel? says plainly that the proverb doesn't rule out mutexes.
A request counter per route is state, not a flow of values:
example.gogotype Stats struct { mu sync.Mutex counts map[string]int } func (s *Stats) Inc(route string) { s.mu.Lock() defer s.mu.Unlock() s.counts[route]++ }
The channel version needs a goroutine that owns the map, a request channel, and a reply channel for reads. It is more code, and every increment has to wait for another goroutine to handle it. For a job queue it is the other way around. A mutex-protected slice needs a sync.Cond or polling to make workers wait for work, while a channel blocks and wakes them for free.
| Use a channel for | Use a mutex for |
|---|---|
| Handing a job or result to another goroutine | Counters, caches and maps updated in place |
| Signaling shutdown, cancellation or completion | Guarding a few fields inside a struct |
| Limiting concurrency or building pipelines | Short critical sections on hot paths |
Test both with go test -race. It flags a map shared without a lock, and it also flags a sender that keeps modifying a struct after sending a pointer to it while the receiver reads it.
Where LevelUpGo fits
LevelUpGo teaches Go through exercises that run real Go code in the browser. Go Concurrency Fundamentals covers goroutines, unbuffered and buffered channels, channel directions, select, context and the race detector, with exercises that deadlock until you fix them. Go Design Patterns builds the worker pool, fan-out and fan-in and graceful shutdown patterns from this post as full exercises. The Training Ground has short standalone concurrency exercises with code that deadlocks, leaks or drops work. For the other 24 reserved words, see Go keywords: all 25 explained.
FAQ
Is chan a keyword in Go?
Yes. chan is one of Go's 25 reserved keywords, so you can't use it as a variable or function name. It appears in channel types such as chan Job, chan<- Job and <-chan Job. The operator <- is not a keyword, and neither is make, which is a predeclared function.
Do you have to close a channel in Go?
No. Close a channel only when receivers need to know that no more values are coming, for example to end a for range loop or to broadcast a shutdown. An unclosed channel that nothing references is garbage collected like any other value.
What is the difference between a buffered and an unbuffered channel?
An unbuffered channel, make(chan T), makes the sender wait until a receiver takes the value, so both goroutines meet. A buffered channel, make(chan T, n), lets up to n values wait in the channel, and the sender only blocks when the buffer is full.
What happens when you read from a closed channel in Go?
You first get any values still in the buffer. After that, every receive returns immediately with the zero value of the element type. The two-value form v, ok := <-ch sets ok to false for those zero values, so you can tell them apart from real ones.
How do you check if a channel is closed in Go?
Receive from it with v, ok := <-ch and check ok. There is no function that reports whether a channel is closed without receiving, and len(ch) only counts buffered values. Code that needs such a check usually has more than one goroutine closing the channel, which is the design to fix.
Can you range over a channel in Go?
Yes. for v := range ch receives values until the channel is closed and empty, then exits the loop. If nobody closes the channel, the loop waits forever, so the sender has to call close when it is done.
Sources
- The Go Programming Language Specification, Channel types: https://go.dev/ref/spec#Channel_types
- The Go Programming Language Specification, Send statements: https://go.dev/ref/spec#Send_statements
- The Go Programming Language Specification, Receive operator: https://go.dev/ref/spec#Receive_operator
- The Go Programming Language Specification, Close: https://go.dev/ref/spec#Close
- The Go Programming Language Specification, Select statements: https://go.dev/ref/spec#Select_statements
- The Go Memory Model, Channel communication: https://go.dev/ref/mem#chan
- Effective Go, Concurrency: https://go.dev/doc/effective_go#concurrency
- The Go Blog, Go Concurrency Patterns: Pipelines and cancellation: https://go.dev/blog/pipelines
- Go Wiki, Use a sync.Mutex or a channel?: https://go.dev/wiki/MutexOrChannel
- Go 1.23 Release Notes, Timer changes: https://go.dev/doc/go1.23#timer-changes
- Go 1.27 Release Notes: https://go.dev/doc/go1.27
- Go Proverbs: https://go-proverbs.github.io/
