Channel
14. Channel(通道)
生活类比
想象一条传送带:一端的人把包裹放上去,另一端的人把包裹取下来。
- 传送带就是 channel
- 放包裹是 发送(
ch <- value) - 取包裹是 接收(
<-ch) - 传送带满了,放的人必须等待;传送带空了,取的人必须等待
这就是 channel 的本质——goroutine 之间安全通信的管道。
核心概念
| 概念 | 说明 |
|---|---|
| channel | 类型化的通信管道,用 chan 关键字声明 |
| 发送 | ch <- value 将数据写入通道 |
| 接收 | value := <-ch 从通道读取数据 |
| 无缓冲 channel | 同步通信,发送和接收必须同时就绪 |
| 有缓冲 channel | 异步通信,缓冲区未满时发送不阻塞 |
| close | 关闭 channel,不再允许发送 |
| range | 从 channel 持续接收直到关闭 |
| 方向限制 | 只发送 chan<- 或只接收 <-chan |
Go 的哲学: Don't communicate by sharing memory; share memory by communicating. 不要通过共享内存来通信,而要通过通信来共享内存。
基本语法与用法
创建 channel
// 无缓冲 channel(同步)
ch := make(chan int)
// 有缓冲 channel(异步,缓冲区大小为 5)
ch := make(chan string, 5)
发送与接收
ch := make(chan int)
// 发送(在另一个 goroutine 中)
go func() {
ch <- 42 // 将 42 发送到 channel
}()
// 接收
value := <-ch // 从 channel 接收值,阻塞直到有数据
fmt.Println(value) // 42
关闭 channel
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch) // 关闭 channel,之后不能再发送
使用 range 接收
ch := make(chan int, 3)
ch <- 10
ch <- 20
ch <- 30
close(ch)
// range 会持续接收直到 channel 关闭
for v := range ch {
fmt.Println(v) // 10 20 30
}
方向限制
// 只能发送的 channel
func producer(ch chan<- int) {
ch <- 100
}
// 只能接收的 channel
func consumer(ch <-chan int) {
v := <-ch
fmt.Println(v)
}
💡 Tips
- 无缓冲 channel 保证发送方和接收方同步——数据直接从一方传递到另一方
- 有缓冲 channel 在缓冲区满时发送会阻塞,在缓冲区空时接收会阻塞
- 向已关闭的 channel 发送数据会引发 panic
- 从已关闭的 channel 接收数据会得到零值,不会阻塞
- 不要从接收方关闭 channel,应该由发送方关闭(除非只有一个发送方)
- 使用
v, ok := <-ch检查 channel 是否已关闭(ok为false表示已关闭且无数据)
示例
示例:基本的 goroutine 通信(难度⭐)
package main
import "fmt"
func main() {
// 创建一个无缓冲 channel
ch := make(chan string)
// 启动 goroutine 发送消息
go func() {
ch <- "你好,主线程!"
}()
// 主 goroutine 接收消息(阻塞等待)
msg := <-ch
fmt.Println(msg) // 你好,主线程!
}
说明: 无缓冲 channel 使两个 goroutine 同步——发送方会阻塞直到接收方准备好。
示例:生产者-消费者模式(难度⭐⭐)
package main
import "fmt"
// 生产者:生成数据并发送到 channel(只能发送)
func producer(id int, ch chan<- int, count int) {
for i := 0; i < count; i++ {
value := id*100 + i
fmt.Printf("生产者 %d: 发送 %d\n", id, value)
ch <- value
}
}
// 消费者:从 channel 接收数据并处理(只能接收)
func consumer(id int, ch <-chan int) {
for v := range ch {
fmt.Printf("消费者 %d: 处理 %d\n", id, v)
}
fmt.Printf("消费者 %d: channel 已关闭,退出\n", id)
}
func main() {
// 创建有缓冲的 channel
ch := make(chan int, 5)
// 启动 2 个生产者
go producer(1, ch, 3)
go producer(2, ch, 3)
// 启动 2 个消费者
go consumer(1, ch)
go consumer(2, ch)
// 等待所有生产者完成(实际项目中应使用 sync.WaitGroup)
// 这里简单用 sleep 演示
import_time := time.After(2 * time.Second)
<-import_time
close(ch) // 关闭 channel
// 给消费者一点时间处理完剩余数据
time.Sleep(500 * time.Millisecond)
fmt.Println("所有工作完成")
}
说明:
- 有缓冲 channel 允许生产者在缓冲区未满时继续生产,不必等待消费者
range ch会持续接收直到 channel 关闭- 方向限制
chan<-和<-chan在编译时防止误用
示例:用 channel 实现信号量与扇出扇入(难度⭐⭐⭐)
package main
import (
"fmt"
"sync"
"time"
)
// worker:处理任务并将结果发送到结果 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: 开始处理任务 %d\n", id, job)
time.Sleep(time.Duration(100+id*50) * time.Millisecond) // 模拟耗时
result := job * job
fmt.Printf("Worker %d: 任务 %d 完成,结果 %d\n", id, job, result)
results <- result
}
}
func main() {
numJobs := 10
numWorkers := 3
jobs := make(chan int, numJobs) // 任务 channel
results := make(chan int, numJobs) // 结果 channel
// 启动 worker 池
var wg sync.WaitGroup
for w := 1; w <= numWorkers; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}
// 发送任务
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs) // 关闭任务 channel,worker 的 range 循环会结束
// 等待所有 worker 完成后关闭结果 channel
go func() {
wg.Wait()
close(results)
}()
// 收集所有结果
total := 0
for r := range results {
total += r
}
fmt.Printf("所有结果之和: %d\n", total)
}
说明:
- 扇出(Fan-out):多个 worker 从同一个 jobs channel 读取任务
- 扇入(Fan-in):多个 worker 将结果写入同一个 results channel
WaitGroup确保所有 worker 完成后再关闭 results channel- 这是 Go 中常见的并发工作池模式
实际应用场景
场景 1:超时控制
package main
import (
"fmt"
"time"
)
func slowOperation(ch chan<- string) {
time.Sleep(3 * time.Second) // 模拟耗时操作
ch <- "操作完成"
}
func main() {
ch := make(chan string, 1)
go slowOperation(ch)
// 使用 select 实现超时控制
select {
case result := <-ch:
fmt.Println("收到结果:", result)
case <-time.After(2 * time.Second):
fmt.Println("操作超时!") // 2 秒后触发
}
}
说明: time.After 返回一个 channel,在指定时间后发送值。结合 select 可以实现优雅的超时控制。
场景 2:并发爬虫的速率限制
package main
import (
"fmt"
"time"
)
// fetchURL 模拟抓取 URL
func fetchURL(url string) string {
time.Sleep(500 * time.Millisecond) // 模拟网络延迟
return "页面内容: " + 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",
}
// 用带缓冲的 channel 作为信号量,限制并发数为 2
semaphore := make(chan struct{}, 2)
results := make(chan string, len(urls))
for _, url := range urls {
go func(u string) {
semaphore <- struct{}{} // 获取信号量(满了就阻塞)
fmt.Printf("开始抓取: %s\n", u)
result := fetchURL(u)
results <- result
<-semaphore // 释放信号量
}(url)
}
// 收集结果
for i := 0; i < len(urls); i++ {
fmt.Println(<-results)
}
fmt.Println("所有页面抓取完成")
}
说明: 带缓冲的 channel 充当信号量,限制同时运行的 goroutine 数量,避免过多并发请求压垮目标服务器。
❓ 常见问题
Q1: 向已关闭的 channel 发送数据会怎样?
会 panic。 任何向已关闭 channel 的发送操作都会触发 panic: send on closed channel。接收操作不会 panic,但会返回零值。
ch := make(chan int, 1)
ch <- 1
close(ch)
// ch <- 2 // ❌ panic: send on closed channel
v := <-ch // ✅ 返回 1(缓冲区中剩余的值)
v2 := <-ch // ✅ 返回 0(零值,channel 已空且已关闭)
最佳实践: 由发送方关闭 channel,而不是接收方。如果多个发送方共享一个 channel,使用 sync.Once 或额外的 done channel 来协调关闭。
Q2: nil channel 会怎样?
发送和接收都会永久阻塞,关闭 nil channel 会 panic。
var ch chan int // nil channel
// ch <- 1 // ❌ 永久阻塞
// v := <-ch // ❌ 永久阻塞
// close(ch) // ❌ panic: close of nil channel
nil channel 在 select 中很有用——对应的 case 会被自动跳过:
var ch chan int // nil channel
select {
case v := <-ch: // 被忽略(nil channel)
fmt.Println(v)
case <-time.After(1 * time.Second):
fmt.Println("超时")
}
Q3: 无缓冲和有缓冲 channel 的区别?
| 特性 | 无缓冲 make(chan T) |
有缓冲 make(chan T, n) |
|---|---|---|
| 发送阻塞条件 | 接收方未准备好 | 缓冲区已满 |
| 接收阻塞条件 | 发送方未准备好 | 缓冲区为空 |
| 同步性 | 同步(会合点) | 异步(直到缓冲满) |
| 典型用途 | 信号通知、同步 | 生产者-消费者、队列 |
Q4: 如何判断 channel 是否已关闭?
使用接收操作的第二个返回值:
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(已关闭且无数据)
ok 为 false 表示 channel 已关闭且缓冲区中没有剩余数据。
📖 小节
| 知识点 | 要点 |
|---|---|
| 创建 channel | make(chan T) 无缓冲,make(chan T, n) 有缓冲 |
| 发送 | ch <- value,阻塞条件取决于缓冲类型 |
| 接收 | value := <-ch 或 v, ok := <-ch |
| 关闭 | close(ch),由发送方负责,关闭后不能发送 |
| range | for v := range ch 持续接收直到关闭 |
| 方向限制 | chan<- 只发送,<-chan 只接收 |
| select | 多路复用,处理多个 channel 操作 |
| 最佳实践 | 优先使用 sync.WaitGroup 等待 goroutine 完成;避免重复关闭 |
Channel 是 Go 并发编程的核心。 理解无缓冲 vs 有缓冲、close/range、方向限制和 select(下一课),你就掌握了 Go 并发通信的精髓。
📝 作业
练习 1:channel 基础
编写一个程序,启动 3 个 goroutine,每个 goroutine 计算一个数的平方并将结果发送到 channel。主 goroutine 接收并打印所有结果。
要求:
- 使用无缓冲 channel
- 使用
WaitGroup确保所有 goroutine 完成 - 输出类似:
结果: [1, 4, 9]
练习 2:用 channel 实现扇入(Fan-in)
编写一个函数 merge(channels ...<-chan int) <-chan int,将多个只接收 channel 合并为一个只接收 channel。
要求:
- 输入:多个
<-chan int - 输出:一个
<-chan int,包含所有输入 channel 的数据 - 使用 goroutine 和
WaitGroup实现 - 当所有输入 channel 关闭后,输出 channel 也关闭
练习 3:带超时的工作池
实现一个工作池,具有以下功能:
- 启动 N 个 worker goroutine
- 每个任务有 500ms 超时
- 如果任务超时,记录并跳过
- 统计成功和超时的任务数
提示: 结合 channel、select 和 time.After 实现。



