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.

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

GO
// Unbuffered channel (synchronous)
ch := make(chan int)

// Buffered channel (asynchronous, buffer size of 5)
ch := make(chan string, 5)

Sending and Receiving

GO
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

GO
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch) // close the channel, no more sending allowed

Receiving with range

GO
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

GO
// Send-only channel
func producer(ch chan<- int) {
    ch <- 100
}

// Receive-only channel
func consumer(ch <-chan int) {
    v := <-ch
    fmt.Println(v)
}

💡 Tips


Examples

Example: Basic Goroutine Communication (Difficulty ⭐)

GO
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!
}
▶ Try it Yourself

Explanation: An unbuffered channel synchronizes two goroutines — the sender blocks until the receiver is ready.


Example: Producer-Consumer Pattern (Difficulty ⭐⭐)

GO
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")
}
▶ Try it Yourself

Explanation:


Example: Semaphore with Channel and Fan-out/Fan-in (Difficulty ⭐⭐⭐)

GO
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)
}
▶ Try it Yourself

Explanation:


Practical Use Cases

Case 1: Timeout Control

GO
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

GO
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.

GO
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.

GO
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:

GO
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:

GO
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:


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:


Exercise 3: Worker Pool with Timeout

Implement a worker pool with the following features:

Hint: Combine channels, select, and time.After.


Next Lesson

15. Select →

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏