Channel
14. Channel
Analogy
Imagine a conveyor belt: one person puts packages on at one end, and another person takes them off at the other end.
- The conveyor belt is the channel
- Putting a package on is sending (
ch <- value) - Taking a package off is receiving (
<-ch) - When the belt is full, the sender must wait; when it's empty, the receiver must wait
This is the essence of a channel — a pipe for safe communication between goroutines.
Core Concepts
| Concept | Description |
|---|---|
| channel | A typed communication pipe, declared with the chan keyword |
| Sending | ch <- value writes data into the channel |
| Receiving | value := <-ch reads data from the channel |
| Unbuffered channel | Synchronous communication; sender and receiver must be ready at the same time |
| Buffered channel | Asynchronous communication; sending doesn't block until the buffer is full |
| close | Closes the channel; no more sending allowed |
| range | Continuously receives from a channel until it's closed |
| Direction restriction | Send-only chan<- or receive-only <-chan |
Go philosophy: Don't communicate by sharing memory; share memory by communicating.
Basic Syntax and Usage
Creating a Channel
// Unbuffered channel (synchronous)
ch := make(chan int)
// Buffered channel (asynchronous, buffer size of 5)
ch := make(chan string, 5)
Sending and Receiving
ch := make(chan int)
// Sending (in another goroutine)
go func() {
ch <- 42 // send 42 to the channel
}()
// Receiving
value := <-ch // receive value from the channel, blocks until data is available
fmt.Println(value) // 42
Closing a Channel
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch) // close the channel, no more sending allowed
Receiving with range
ch := make(chan int, 3)
ch <- 10
ch <- 20
ch <- 30
close(ch)
// range continues receiving until the channel is closed
for v := range ch {
fmt.Println(v) // 10 20 30
}
Direction Restrictions
// Send-only channel
func producer(ch chan<- int) {
ch <- 100
}
// Receive-only channel
func consumer(ch <-chan int) {
v := <-ch
fmt.Println(v)
}
💡 Tips
- Unbuffered channels guarantee synchronization between sender and receiver — data is transferred directly from one to the other
- Buffered channels block on send when the buffer is full, and block on receive when the buffer is empty
- Sending to a closed channel causes a panic
- Receiving from a closed channel returns the zero value without blocking
- Don't close a channel from the receiver side; the sender should close it (unless there's only one sender)
- Use
v, ok := <-chto check if a channel is closed (okisfalsewhen closed and no data remains)
Examples
Example: Basic Goroutine Communication (Difficulty ⭐)
package main
import "fmt"
func main() {
// Create an unbuffered channel
ch := make(chan string)
// Launch a goroutine to send a message
go func() {
ch <- "Hello, main thread!"
}()
// Main goroutine receives the message (blocks while waiting)
msg := <-ch
fmt.Println(msg) // Hello, main thread!
}
Explanation: An unbuffered channel synchronizes two goroutines — the sender blocks until the receiver is ready.
Example: Producer-Consumer Pattern (Difficulty ⭐⭐)
package main
import "fmt"
// Producer: generates data and sends to channel (send-only)
func producer(id int, ch chan<- int, count int) {
for i := 0; i < count; i++ {
value := id*100 + i
fmt.Printf("Producer %d: sending %d\n", id, value)
ch <- value
}
}
// Consumer: receives data from channel and processes it (receive-only)
func consumer(id int, ch <-chan int) {
for v := range ch {
fmt.Printf("Consumer %d: processing %d\n", id, v)
}
fmt.Printf("Consumer %d: channel closed, exiting\n", id)
}
func main() {
// Create a buffered channel
ch := make(chan int, 5)
// Start 2 producers
go producer(1, ch, 3)
go producer(2, ch, 3)
// Start 2 consumers
go consumer(1, ch)
go consumer(2, ch)
// Wait for all producers to finish (in real projects, use sync.WaitGroup)
// Here we simply use sleep for demonstration
import_time := time.After(2 * time.Second)
<-import_time
close(ch) // close the channel
// Give consumers time to process remaining data
time.Sleep(500 * time.Millisecond)
fmt.Println("All work completed")
}
Explanation:
- Buffered channels allow producers to continue producing as long as the buffer isn't full, without waiting for consumers
range chcontinues receiving until the channel is closed- Direction restrictions
chan<-and<-chanprevent misuse at compile time
Example: Semaphore with Channel and Fan-out/Fan-in (Difficulty ⭐⭐⭐)
package main
import (
"fmt"
"sync"
"time"
)
// worker processes tasks and sends results to the results channel
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
fmt.Printf("Worker %d: started processing task %d\n", id, job)
time.Sleep(time.Duration(100+id*50) * time.Millisecond) // simulate work
result := job * job
fmt.Printf("Worker %d: task %d completed, result %d\n", id, job, result)
results <- result
}
}
func main() {
numJobs := 10
numWorkers := 3
jobs := make(chan int, numJobs) // task channel
results := make(chan int, numJobs) // result channel
// Start worker pool
var wg sync.WaitGroup
for w := 1; w <= numWorkers; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}
// Send tasks
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs) // close the task channel; workers' range loops will end
// Close the results channel after all workers finish
go func() {
wg.Wait()
close(results)
}()
// Collect all results
total := 0
for r := range results {
total += r
}
fmt.Printf("Sum of all results: %d\n", total)
}
Explanation:
- Fan-out: multiple workers read from the same jobs channel
- Fan-in: multiple workers write to the same results channel
WaitGroupensures all workers finish before closing the results channel- This is a common concurrent worker pool pattern in Go
Practical Use Cases
Case 1: Timeout Control
package main
import (
"fmt"
"time"
)
func slowOperation(ch chan<- string) {
time.Sleep(3 * time.Second) // simulate a slow operation
ch <- "Operation complete"
}
func main() {
ch := make(chan string, 1)
go slowOperation(ch)
// Use select for timeout control
select {
case result := <-ch:
fmt.Println("Received result:", result)
case <-time.After(2 * time.Second):
fmt.Println("Operation timed out!") // triggers after 2 seconds
}
}
Explanation: time.After returns a channel that sends a value after the specified duration. Combined with select, it enables elegant timeout control.
Case 2: Rate Limiting for Concurrent Crawlers
package main
import (
"fmt"
"time"
)
// fetchURL simulates fetching a URL
func fetchURL(url string) string {
time.Sleep(500 * time.Millisecond) // simulate network latency
return "Page content: " + url
}
func main() {
urls := []string{
"https://example.com/page1",
"https://example.com/page2",
"https://example.com/page3",
"https://example.com/page4",
"https://example.com/page5",
}
// Use a buffered channel as a semaphore, limiting concurrency to 2
semaphore := make(chan struct{}, 2)
results := make(chan string, len(urls))
for _, url := range urls {
go func(u string) {
semaphore <- struct{}{} // acquire semaphore (blocks when full)
fmt.Printf("Start fetching: %s\n", u)
result := fetchURL(u)
results <- result
<-semaphore // release semaphore
}(url)
}
// Collect results
for i := 0; i < len(urls); i++ {
fmt.Println(<-results)
}
fmt.Println("All pages fetched")
}
Explanation: A buffered channel acts as a semaphore, limiting the number of concurrently running goroutines and preventing too many concurrent requests from overwhelming the target server.
❓ FAQ
Q1: What happens if you send data to a closed channel?
It panics. Any send operation on a closed channel triggers panic: send on closed channel. Receiving won't panic but returns the zero value.
ch := make(chan int, 1)
ch <- 1
close(ch)
// ch <- 2 // ❌ panic: send on closed channel
v := <-ch // ✅ returns 1 (remaining value in buffer)
v2 := <-ch // ✅ returns 0 (zero value, channel is empty and closed)
Best practice: The sender should close the channel, not the receiver. If multiple senders share a channel, use sync.Once or an additional done channel to coordinate closing.
Q2: What about a nil channel?
Sending and receiving both block permanently; closing a nil channel panics.
var ch chan int // nil channel
// ch <- 1 // ❌ blocks permanently
// v := <-ch // ❌ blocks permanently
// close(ch) // ❌ panic: close of nil channel
A nil channel is useful in select — the corresponding case is automatically skipped:
var ch chan int // nil channel
select {
case v := <-ch: // ignored (nil channel)
fmt.Println(v)
case <-time.After(1 * time.Second):
fmt.Println("Timed out")
}
Q3: What's the difference between unbuffered and buffered channels?
| Feature | Unbuffered make(chan T) |
Buffered make(chan T, n) |
|---|---|---|
| Send blocks when | Receiver not ready | Buffer is full |
| Receive blocks when | Sender not ready | Buffer is empty |
| Synchronicity | Synchronous (rendezvous) | Asynchronous (until buffer full) |
| Typical use | Signal notification, sync | Producer-consumer, queue |
Q4: How do you check if a channel is closed?
Use the second return value of the receive operation:
ch := make(chan int, 1)
ch <- 42
close(ch)
v, ok := <-ch
fmt.Println(v, ok) // 42 true
v2, ok := <-ch
fmt.Println(v2, ok) // 0 false (closed and no data)
ok is false when the channel is closed and there's no remaining data in the buffer.
📖 Summary
| Key Point | Detail |
|---|---|
| Creating a channel | make(chan T) for unbuffered, make(chan T, n) for buffered |
| Sending | ch <- value, blocking condition depends on buffer type |
| Receiving | value := <-ch or v, ok := <-ch |
| Closing | close(ch), sender's responsibility, cannot send after closing |
| range | for v := range ch continues receiving until closed |
| Direction restriction | chan<- send-only, <-chan receive-only |
| select | Multiplexing, handles multiple channel operations |
| Best practice | Prefer sync.WaitGroup to wait for goroutines to complete; avoid double closing |
Channels are the core of Go concurrency. Understanding unbuffered vs buffered, close/range, direction restrictions, and select (next lesson), and you'll have mastered the essence of Go's concurrent communication.
📝 Exercises
Exercise 1: Channel Basics
Write a program that starts 3 goroutines, each computing the square of a number and sending the result to a channel. The main goroutine receives and prints all results.
Requirements:
- Use an unbuffered channel
- Use
WaitGroupto ensure all goroutines complete - Output should look like:
Results: [1, 4, 9]
Exercise 2: Implement Fan-in with Channels
Write a function merge(channels ...<-chan int) <-chan int that merges multiple receive-only channels into one receive-only channel.
Requirements:
- Input: multiple
<-chan int - Output: one
<-chan intcontaining data from all input channels - Implement using goroutines and
WaitGroup - When all input channels are closed, the output channel should also close
Exercise 3: Worker Pool with Timeout
Implement a worker pool with the following features:
- Start N worker goroutines
- Each task has a 500ms timeout
- If a task times out, log it and skip
- Count successful and timed-out tasks
Hint: Combine channels, select, and time.After.



