ch := make(chanint)
gofunc() {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}()
for v := range ch {
fmt.Println(v) // 打印 0~9,通道关闭后自动退出循环
}
9.2 使用 select 多路复用
select 语句让一个 goroutine 可以同时等待多个通道操作。
select {
case v := <-ch1:
fmt.Println("收到 ch1:", v)
case v := <-ch2:
fmt.Println("收到 ch2:", v)
case ch3 <- 42:
fmt.Println("向 ch3 发送 42")
default:
fmt.Println("无任何通道就绪")
}
select 随机选择一个可用的 case 执行;如果多个同时就绪,随机选择。
如果所有 case 都阻塞,且没有 default,则 select 阻塞直到某个 case 就绪。
空 select{} 会永久阻塞(通常用于防止 main 退出)。
default 子句使 select 变为非阻塞。
9.3 超时控制
结合 time.After 实现超时:
select {
case v := <-ch:
fmt.Println(v)
case <-time.After(5 * time.Second):
fmt.Println("超时")
}
9.4 工作池(Worker Pool)
使用带缓冲通道作为任务队列:
funcworker(id int, jobs <-chanint, results chan<- int) {
for job := range jobs {
results <- job * 2// 模拟处理
}
}
funcmain() {
const numJobs = 100
jobs := make(chanint, numJobs)
results := make(chanint, numJobs)
// 启动 3 个 workerfor w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// 发送任务for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
// 收集结果for a := 1; a <= numJobs; a++ {
<-results
}
}
9.5 扇出/扇入(Fan-out/Fan-in)
扇出:从一个通道分发到多个 goroutine 处理。
扇入:将多个输入通道合并到一个输出通道。
// 扇入示例funcfanIn(ch1, ch2 <-chanstring) <-chanstring {
out := make(chanstring)
gofunc() {
for {
select {
case v := <-ch1:
out <- v
case v := <-ch2:
out <- v
}
}
}()
return out
}
var ch chanintselect {
case v := <-ch: // 由于 ch 为 nil,此 case 永远阻塞default:
fmt.Println("不会走到这里,因为 default 先执行?") // 注意:如果所有 case 阻塞,且有 default,则执行 default
}
实际中,可通过置 nil 来'禁用'某个 case。
11.3 空 select
select {} 会永久阻塞,通常用于让 main goroutine 等待其他 goroutine,但此时其他 goroutine 必须能持续运行,否则整个程序死锁。
var tokens = make(chanstruct{}, 10) // 最多允许 10 个并发funcwork() {
tokens <- struct{}{} // 获取令牌deferfunc() { <-tokens }() // do work
}
13. 与其他并发原语的对比
原语
适用场景
特点
channel
goroutine 间通信,传递数据,信号同步
类型安全,内置同步,适合数据流和事件通知
sync.Mutex
保护共享内存,临界区访问
轻量级,低开销,适合简单的互斥
sync.RWMutex
读多写少的共享资源保护
允许多个读,单个写
sync.WaitGroup
等待一组 goroutine 完成
简单计数器,常用于等待任务结束
sync.Once
确保某个函数只执行一次
初始化单例等
sync.Cond
复杂的条件等待(多个条件变量)
与 Mutex 配合使用,用于 goroutine 等待特定条件
atomic
简单的整数/指针原子操作,无锁编程
高性能,但仅支持基本类型,无法保护复杂结构
何时使用 channel?
需要传递数据所有权。
需要生产者 - 消费者模型。
需要超时、取消、广播通知。
需要将并发逻辑组合成流水线。
何时使用 Mutex?
保护共享结构体内部状态。
简单的计数器或标志位(虽然 atomic 更合适)。
性能敏感且无法用 channel 无阻塞实现。
14. 高级话题
14.1 使用通道实现取消(context 原理)
context 包底层使用通道来传递取消信号。一个简单的取消实现:
funcworker(stop <-chanstruct{}) {
for {
select {
case <-stop:
returndefault:
// 工作
}
}
}
stop := make(chanstruct{})
go worker(stop)
// 当需要停止时 close(stop)
14.2 使用通道实现流水线(pipeline)
将问题分解为多个阶段,每个阶段通过通道连接,实现并发流水线。
// 生成数字funcgen(nums ...int) <-chanint {
out := make(chanint)
gofunc() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
// 平方funcsq(in <-chanint) <-chanint {
out := make(chanint)
gofunc() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
funcmain() {
for v := range sq(sq(gen(2, 3))) {
// 2->4, 3->9 再平方?实际上 sq(sq(gen)) 是对每个数两次平方
fmt.Println(v)
}
}
14.3 使用通道实现并发循环
通过固定数量的 worker 处理大量任务,如工作池。
14.4 使用通道实现速率限制
利用 time.Ticker 或带缓冲通道控制请求速率。
limiter := time.Tick(200 * time.Millisecond) // 每秒 5 个for req := range requests {
<-limiter
go process(req)
}