写Go程序时,channel是协程间通信的主力。无缓冲channel像一根细水管,两边必须同时准备好才能传数据;而缓冲channel就像带水箱的管道,能暂存几条消息,让生产者和消费者节奏错开——这在真实项目里特别管用。
场景1:批量日志收集
后端服务每秒产生几十条日志,直接写磁盘太慢,还可能拖垮主逻辑。开个缓冲channel(比如容量100),所有goroutine往里塞日志,另起一个goroutine定时从channel取一批写入文件:
logCh := make(chan string, 100)
// 日志生产者
go func() {
for i := 0; i < 1000; i++ {
logCh <- fmt.Sprintf("[INFO] request %d done", i)
}
}()
// 日志消费者
go func() {
batch := make([]string, 0, 50)
for log := range logCh {
batch = append(batch, log)
if len(batch) >= 50 {
writeToFile(batch)
batch = batch[:0]
}
}
if len(batch) > 0 {
writeToFile(batch)
}
}()场景2:限流任务分发
调用第三方API有每秒10次限制,但内部请求可能瞬间涌来50个。用容量为10的缓冲channel做“漏斗”,配合time.Ticker控制消费节奏:
taskCh := make(chan Task, 10)
// 所有任务先入队
go func() {
for _, t := range allTasks {
taskCh <- t // 不阻塞,只要没满就行
}
close(taskCh)
}()
// 每100ms取一个执行
ticker := time.NewTicker(100 * time.Millisecond)
for range ticker.C {
select {
case task, ok := <-taskCh:
if !ok { return }
callExternalAPI(task)
default:
// 队列空了就退出
return
}
}场景3:异步通知不丢消息
用户注册成功后要发邮件、推站内信、更新统计,三个动作耗时不同。用缓冲channel接收通知事件,即使某个下游暂时卡住,也不会让注册接口超时:
notifyCh := make(chan Notification, 20)
// 注册主流程只管发
func registerUser() {
// ...创建用户逻辑
notifyCh <- Notification{UserID: 123, Type: "welcome"}
}
// 后台慢慢处理
go func() {
for n := range notifyCh {
sendEmail(n)
sendPush(n)
updateStats(n)
}
}()场景4:防止goroutine爆炸
遍历1000个URL做健康检查,如果每个都起goroutine又不加控制,瞬间几百个协程可能压垮本地网络栈。用缓冲channel当“协程池”入口:
urlCh := make(chan string, 10) // 最多缓存10个待检URL
// 控制并发数为5
for i := 0; i < 5; i++ {
go func() {
for url := range urlCh {
checkHealth(url)
}
}()
}
// 发送URL,超10个会阻塞,自然限速
for _, u := range urls {
urlCh <- u
}
close(urlCh)场景5:解耦读写速度差异
传感器每毫秒上报一次温度数据,但数据库写入平均要3毫秒。无缓冲channel会立刻堵死采集goroutine。换成容量100的缓冲channel,采集方几乎不等待,数据在内存里暂存几秒再落库:
tempCh := make(chan float64, 100)
// 采集goroutine(快)
go func() {
for {
t := readSensor()
tempCh <- t // 几乎不阻塞
time.Sleep(time.Millisecond)
}
}()
// 写库goroutine(慢)
go func() {
for t := range tempCh {
saveToDB(t) // 可能花几毫秒
}
}()缓冲channel不是万能胶,容量设太大容易吃光内存,设太小又起不到缓冲作用。常见做法是根据峰值QPS × 平均处理延迟粗略估算,比如每秒100请求、单次处理200ms,缓冲10~20就比较稳。