Channels
By Syncfusion®, Inc. · Last updated
Go channels: create, send and receive, buffered channels and blocking, close, range and select, timeouts, channel directions, worker pools and done signals.
What Are Channels?
Channel = a pipe that goroutines use to send values to each other.
- One goroutine sends a value into the channel.
- Another goroutine receives it from the other end.
- The channel makes them wait for each other at the right moment.
Go's motto: "Do not communicate by sharing memory. Share memory by communicating."
Simple lesson: a channel is a safe pipe for passing values between goroutines.
A Real-World Example
Think of the answer-sheet tray between the exam hall and the marking room.
- Invigilators drop answer sheets into the tray.
- Teachers pick sheets out of the tray to mark.
- If the tray is empty, a teacher waits.
- If the tray is full, an invigilator waits.
- At the end, the office puts up a sign: "No more sheets". That is closing the channel.
Now the mapping:
- The tray = the channel.
- Dropping a sheet =
ch <- sheet(send). - Picking a sheet =
sheet := <-ch(receive). - The tray's size = the channel's buffer capacity.
You met goroutines in Goroutines.
There, each goroutine wrote to its own slot and main used a WaitGroup. Channels let goroutines hand values to each other instead.
What You Will Learn
In this lesson, you will learn:
- How to create channels, and send and receive values.
- Buffered channels: capacity and blocking.
- How to close a channel and loop over it with
range. - How
selectwaits on many channels at once. - Patterns for goroutine communication and data sharing.
Channel Fundamentals
Creating Channels
ch := make(chan string) // unbuffered channel of strings
nums := make(chan int, 3) // buffered: holds up to 3 ints
chan T= a channel that carries values of typeT.- Channels are made with
make. The zero value of a channel isnil.
Sending and Receiving Data
| Action | Code |
|---|---|
| Send | ch <- value |
| Receive into a variable | v := <-ch |
| Receive and throw away | <-ch |
- The arrow shows which way the value moves.
Send from a goroutine, receive in main
package main
import "fmt"
func main() {
results := make(chan string)
go func() {
results <- "Asha scored 91"
}()
msg := <-results
fmt.Println(msg)
}
Asha scored 91
mainwaits at<-resultsuntil the goroutine sends.- No WaitGroup is needed: receiving the value is the waiting.
Unbuffered Channels Hand Over Directly
Unbuffered channel = a channel with no storage. A send waits until a receiver takes the value.
Try sending and receiving. Switch between unbuffered and buffered.
Watch for BLOCKED:
- Unbuffered: a send waits for a receiver, and a receive waits for a sender.
- Buffered: a send waits only when the buffer is full.
Simple lesson: ch <- v sends, <-ch receives, and both wait until the other side is ready.
Buffered Channels
Capacity
Buffered channel = a channel with room for some values: make(chan T, n).
A buffer of 3
package main
import "fmt"
func main() {
tray := make(chan string, 3)
tray <- "sheet 1"
tray <- "sheet 2"
fmt.Println(len(tray), cap(tray))
fmt.Println(<-tray)
fmt.Println(<-tray)
fmt.Println(len(tray))
}
2 3
sheet 1
sheet 2
0
cap(ch)= how many values fit.len(ch)= how many are waiting right now.- Values come out in the order they went in: first in, first out.
Blocking Behavior
Block = wait at that line until something happens.
| Channel | Send blocks when | Receive blocks when |
|---|---|---|
| Unbuffered | no receiver is ready | no sender is ready |
| Buffered | the buffer is full | the buffer is empty |
Deadlock
Deadlock = every goroutine is waiting, and none can ever continue.
func main() {
ch := make(chan int)
ch <- 1 // waits for a receiver... that never comes
fmt.Println(<-ch)
}
// fatal error: all goroutines are asleep - deadlock!
- The send waits forever, because the receive is on the next line of the same goroutine.
- The real
gotool stops with the message above. - In this lab, a deadlocked program is stopped after a few seconds instead.
Simple lesson: a buffered channel lets a sender run ahead until the buffer is full.
Channel Operations
close()
close(ch) = say "no more values will be sent".
- Only the sender should close a channel.
- Receiving from a closed, empty channel gives the zero value at once.
- Sending on a closed channel panics.
Receiving with ok
Is the channel closed?
package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 91
close(ch)
v, ok := <-ch
fmt.Println(v, ok)
v, ok = <-ch
fmt.Println(v, ok)
}
91 true
0 false
okisfalsewhen the channel is closed and empty.
range
for v := range ch receives until the channel is closed and empty. See Loops.
Press Play to watch a producer and a consumer.
Produce, close, and range
- Without
close, therangewould wait forever after the last sheet: a deadlock.
select
select = wait on several channel operations, and run whichever is ready first.
Whichever answers first
package main
import (
"fmt"
"time"
)
func main() {
library := make(chan string)
canteen := make(chan string)
go func() {
time.Sleep(30 * time.Millisecond)
library <- "book is ready"
}()
go func() {
time.Sleep(10 * time.Millisecond)
canteen <- "lunch is ready"
}()
for i := 0; i < 2; i++ {
select {
case m := <-library:
fmt.Println("library:", m)
case m := <-canteen:
fmt.Println("canteen:", m)
}
}
}
canteen: lunch is ready
library: book is ready
- Each
selectwaits until one case can go ahead, then runs it. - The canteen answers first, so its case runs first.
Timeouts with select
Do not wait forever
package main
import (
"fmt"
"time"
)
func main() {
reply := make(chan string)
go func() {
time.Sleep(200 * time.Millisecond)
reply <- "too late"
}()
select {
case r := <-reply:
fmt.Println(r)
case <-time.After(50 * time.Millisecond):
fmt.Println("timed out after 50ms")
}
}
timed out after 50ms
time.After(d)gives a channel that receives a value afterd.- A
default:case runs at once if no other case is ready, soselectnever waits.
Channel operations at a glance
Simple lesson: close ends a stream, range reads it to the end, and select waits on many channels at once.
Channel Communication Patterns
Goroutine Communication
Worker pool = a few goroutines that take jobs from one channel and put results on another.
Three workers mark ten papers
package main
import (
"fmt"
"sort"
)
type result struct {
roll int
marks int
}
func worker(jobs <-chan int, results chan<- result) {
for roll := range jobs {
results <- result{roll, 50 + roll*4}
}
}
func main() {
jobs := make(chan int, 10)
results := make(chan result, 10)
for w := 1; w <= 3; w++ {
go worker(jobs, results)
}
for roll := 1; roll <= 10; roll++ {
jobs <- roll
}
close(jobs)
var all []result
for i := 0; i < 10; i++ {
all = append(all, <-results)
}
sort.Slice(all, func(i, j int) bool { return all[i].roll < all[j].roll })
fmt.Println(all[0], all[9], len(all))
}
{1 54} {10 90} 10
<-chan int= a receive-only channel.chan<- result= a send-only channel.- These direction types stop a worker from using a channel the wrong way.
- Results arrive in any order, so they are sorted before printing.
Data Sharing
- Pass ownership: when a value is sent, the sender should stop using it. The receiver now owns it.
- One writer: let one goroutine own a piece of data, and let others send it requests through a channel.
- Signal "done": a
chan struct{}withclose(done)tells many goroutines to stop at once.
A done channel
package main
import "fmt"
func main() {
done := make(chan struct{})
finished := make(chan string)
go func() {
<-done
finished <- "cleaner stopped"
}()
close(done)
fmt.Println(<-finished)
}
cleaner stopped
struct{}carries no data. The signal is the close itself.- Every goroutine waiting on
<-donewakes up when it is closed.
Common channel traps:
- Sending on a closed channel panics.
- Closing a channel twice panics.
- Receiving from a
nilchannel waits forever.
Easy way to remember
ch <- v= put in.<-ch= take out.- Unbuffered = hand to hand. Buffered = a tray of size n.
- The sender closes. The receiver ranges.
Simple lesson: channels connect goroutines: jobs go in, results come out, and closing a channel signals the end.
TL;DR
- Channel =
make(chan T)ormake(chan T, n). A pipe between goroutines. - Send =
ch <- v. Receive =v := <-ch. Unbuffered = sender and receiver meet. Buffered = room for
nvalues.- Blocking = waiting until the other side is ready.
- Deadlock = everyone waits forever.
- close(ch) = no more values.
v, ok := <-ch→okis false when closed and empty. - range =
for v := range chuntil closed. - select = wait on many channels.
time.Afterfor timeouts. - Directions =
chan<- Tsend-only,<-chan Treceive-only. - Patterns = worker pool, pass ownership, done channel.
Common mistakes
| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Send and receive in the same goroutine on an unbuffered channel | It hands itself the value | Another goroutine must receive | Deadlock. |
Forgetting close before range | range stops by itself | It waits until the channel is closed | Deadlock at the end. |
| The receiver closes the channel | Anyone can close | Only the sender closes | send on closed channel panic. |
| Closing twice | It is harmless | It panics | close of closed channel |
select with no timeout on a slow channel | It always answers | It may wait forever | A stuck program. Add time.After. |
| Expecting worker results in order | They come in send order | Any order | Sort or index the results. |
Check Yourself
Exercises
What to Remember
- A channel passes values between goroutines:
ch <- v,v := <-ch. - Unbuffered channels hand over directly. Buffered ones hold up to
nvalues. - The sender closes.
rangereads until closed. selectwaits on many channels, andtime.Afteradds a timeout.- Worker pools: jobs in, results out, and sort results if order matters.
Next: Synchronization. You will learn how to protect shared data with WaitGroups, mutexes and atomic operations.