Go Channels — Unbuffered Buffered Range and Directional Channels Explained
In this tutorial, you will learn about Go Channels. We cover key concepts, practical examples, and best practices to help you master this topic.
Go channels provide typed communication between goroutines with unbuffered channels for synchronization, buffered channels for async handoff, range for channel iteration, and directional channel types for API safety.
What You'll Learn
- Creating and using channels
- Unbuffered vs buffered channels
- Channel direction
- Range and close patterns
Why It Matters
Channels are Go's primary concurrency primitive. Docker uses channels for stream handling. Kubernetes uses channels for watch APIs and event processing. DodaZIP uses channels for progress reporting.
Real-World Use
HTTP server request pipelines, worker pools, event processing systems, data streaming, pipeline architectures — all use channels for Goroutine communication.
flowchart LR
A["Channels"] --> B["Unbuffered"]
B --> C["Buffered"]
C --> D["Directional"]
D --> E["Range/Close"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Basic Channel Operations
func main() {
ch := make(chan int)
go func() { ch <- 42 }()
val := <-ch
fmt.Println(val)
}
Unbuffered Channels
func main() {
ch := make(chan string)
go func() {
fmt.Println("Goroutine: sending...")
ch <- "hello"
fmt.Println("Goroutine: sent!")
}()
time.Sleep(100 * time.Millisecond)
fmt.Println("Main: receiving...")
msg := <-ch
fmt.Println("Main: received", msg)
}
// Goroutine: sending...
// Main: receiving...
// Main: received hello
// Goroutine: sent!
Buffered Channels
func main() {
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
}
Range over Channel
func generate(count int) <-chan int {
ch := make(chan int)
go func() {
for i := 1; i <= count; i++ {
ch <- i
}
close(ch)
}()
return ch
}
func main() {
for num := range generate(5) {
fmt.Println("Received:", num)
}
}
Directional Channels
func producer(ch chan<- int) {
for i := 0; i < 5; i++ { ch <- i }
close(ch)
}
func consumer(ch <-chan int) {
for val := range ch {
fmt.Println("Consumed:", val)
}
}
func main() {
ch := make(chan int, 5)
go producer(ch)
consumer(ch)
}
Select Statement
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(100 * time.Millisecond)
ch1 <- "from ch1"
}()
go func() {
time.Sleep(200 * time.Millisecond)
ch2 <- "from ch2"
}()
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
case <-time.After(50 * time.Millisecond):
fmt.Println("timeout")
}
}
Fan-Out Pattern
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job)
time.Sleep(time.Second)
results <- job * 2
}
}
func main() {
const numJobs = 10
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
for r := 1; r <= numJobs; r++ {
<-results
}
}
Fan-In Pattern
func producer(name string, count int) <-chan string {
ch := make(chan string)
go func() {
for i := 0; i < count; i++ {
ch <- fmt.Sprintf("%s: %d", name, i)
time.Sleep(100 * time.Millisecond)
}
close(ch)
}()
return ch
}
func fanIn(chs ...<-chan string) <-chan string {
out := make(chan string)
var wg sync.WaitGroup
for _, ch := range chs {
wg.Add(1)
go func(c <-chan string) {
defer wg.Done()
for msg := range c {
out <- msg
}
}(ch)
}
go func() { wg.Wait(); close(out) }()
return out
}
func main() {
combined := fanIn(producer("A", 3), producer("B", 3))
for msg := range combined {
fmt.Println(msg)
}
}
Pipeline Pattern
func nums(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums { out <- n }
close(out)
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in { out <- n * n }
close(out)
}()
return out
}
func main() {
for r := range square(square(nums(1, 2, 3))) {
fmt.Println(r)
}
}
Common Mistakes
1. Send on Unbuffered Without Receiver
ch := make(chan int)
ch <- 42 // Deadlock! No receiver
2. Closing Channel Twice
ch := make(chan int)
close(ch)
close(ch) // Panic
3. Send on Closed Channel
ch := make(chan int)
close(ch)
ch <- 42 // Panic
4. Not Closing Channel
ch := make(chan int)
go func() { ch <- 1 /* missing close */ }()
for v := range ch { } // Never exits
5. Reading from nil Channel
var ch chan int // nil
ch <- 42 // Blocks forever
<-ch // Blocks forever
Practice Questions
1. What's the difference between buffered and unbuffered channels? Unbuffered synchronize — send blocks until receive. Buffered allow sending up to capacity without blocking.
2. When does range over a channel exit? When the channel is closed with close(ch). Only the sender should close.
3. What is a directional channel? A channel restricted to send-only (chan<-) or receive-only (<-chan). Compile-time safety.
4. What does select do? Choose which of multiple channel operations proceeds. Like switch for channels.
Challenge: Build a concurrent pipeline that reads integers, doubles them in parallel workers, and collects results.
Solution
func stage(in <-chan int, fn func(int) int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- fn(n)
}
close(out)
}()
return out
}
func main() {
input := make(chan int)
go func() {
for i := 1; i <= 10; i++ { input <- i }
close(input)
}()
doubled := stage(stage(input, func(n int) int { return n * 2 }),
func(n int) int { return n * 2 })
for r := range doubled {
fmt.Println(r)
}
}
FAQ
{{< faq question="Should I use channels or mutexes?" >}} Channels communicate data between goroutines. Mutexes protect shared state. "Share memory by communicating, don't communicate by sharing memory." Prefer channels for data flow, mutexes for state protection. {{< /faq >}}
{{< faq question="Can I close a channel more than once?" >}} No. Closing a closed channel panics. Use sync.Once if you must close from multiple sources. {{< /faq >}}
{{< faq question="What is the zero value of a channel?" >}} nil. Sending to or receiving from a nil channel blocks forever. Used intentionally in select statements to disable cases. {{< /faq >}}
{{< faq question="How do I check if a channel is closed?" >}}
Use the two-value receive: val, ok := <-ch. ok is false if the channel is closed. Or use range, which exits on close.
{{< /faq >}}
{{< faq question="What buffer size should I use?" >}} Unbuffered (0) for synchronization. Small buffer (1-100) for handoff. Large buffers can mask design issues. Profile to find the right size. {{< /faq >}}
Try It Yourself
package main
import "fmt"
func main() {
ch := make(chan int, 5)
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}()
for v := range ch {
fmt.Printf("Received: %d\n", v)
}
val, ok := <-ch
fmt.Printf("Closed: val=%d, ok=%v\n", val, ok)
}
Expected output:
Received: 0
Received: 1
Received: 2
Received: 3
Received: 4
Closed: val=0, ok=false
What's Next
Now that you understand channels, learn about the select statement for multiplexing channel operations.
| Topic | Description | Link |
|---|---|---|
| Go Select | Multiplexing, timeouts, default | {{< ref "19-select" >}} |
| Go WaitGroups | Synchronization patterns | {{< ref "20-waitgroups" >}} |
| Rust Channels | Compare Rust's mpsc channels | Rust |