channel 与任务协调
channel 与任务协调 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
这课用 channel 把一批书签 URL 的校验做成小型工作流水线:N 个 worker 并发消费任务,理解发送、接收、关闭三方的责任划分,能识别阻塞、提前退出和泄漏这三类典型事故。
问题场景
导入 2000 条书签时要逐个检查 URL 是否可访问,串行一次一个太慢。并发能提速,但马上遇到协调问题:任务怎么分?结果怎么收?谁先结束?channel 是 Go 的答案——它不只是队列,更是「交接所有权」的约定。
核心解释
经典的 fan-out/fan-in 结构:
type checkResult struct {
URL string
OK bool
}
func checkAll(ctx context.Context, urls []string, workers int) []checkResult {
jobs := make(chan string)
results := make(chan checkResult)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for url := range jobs { // jobs 关闭后循环自然结束
results <- check(url)
}
}()
}
go func() {
defer close(jobs) // 只有发送方关闭
for _, u := range urls {
select {
case jobs <- u:
case <-ctx.Done():
return
}
}
}()
go func() {
wg.Wait()
close(results) // 所有 worker 结束后才关结果
}()
var out []checkResult
for r := range results {
out = append(out, r)
}
return out
}
责任划分是本课的魂:close 只能由发送方调用(接收方关发送侧是设计灾难);worker 用 for range jobs 消费,jobs 关闭后循环自动退出,不需要数数;results 由收尾 goroutine 在全部 worker 结束后关闭,主流程 for range results 收完自然结束。三组 goroutine、两个关闭点,各归其位。
两个易错点藏在细节里。提交任务的 select 带 ctx.Done() 分支:如果下游 worker 全卡住而 ctx 又被取消,不带这个分支发送方会永远阻塞在 jobs <- u,上游泄漏。反过来,主流程提前 return 不再收 results,worker 也会永久阻塞在发送上——「谁可能阻塞,谁就要有退出出口」,这是检查 channel 代码的口诀。
无缓冲 channel(make(chan T))发送会等接收方就绪,像当面交接;带缓冲 make(chan T, n) 像放货架,满之前不等人。工作队列通常无缓冲就够:缓冲不解决慢 worker,只是延迟堵车。
示例拆解
check 用 HEAD 请求简化(真实实现 G12 会加超时):
func check(url string) checkResult {
resp, err := http.Head(url)
if err != nil {
return checkResult{URL: url, OK: false}
}
resp.Body.Close()
return checkResult{URL: url, OK: resp.StatusCode < 400}
}
main 里准备 20 个 URL(一半故意写错域名),workers 分别设 1、4、16 各跑一次并打印耗时:worker 少时加速明显,多到超过目标站点承载反而失败率上升——并发度要匹配资源,不是越大越好。收尾统计用标准库新工具很顺手:
counts := map[string]int{}
for _, r := range out {
if r.OK {
counts["ok"]++
} else {
counts["fail"]++
}
}
fmt.Println(counts) // map[fail:10 ok:10]
动手任务
- 把 workers 从固定值改成 min(len(urls), 8),避免 3 个任务开 8 个 goroutine 的浪费。
- 故意删掉 close(jobs) 那行,go run . 观察程序永不结束——亲眼看一次泄漏形态,再改回来。
- 给 checkResult 加 Err 字段,把网络错误和状态码错误分开收集,体会结果与失败在类型上的区分。
常见错误
- 接收方调用 close:panic(close of closed channel)或违反所有权约定,关闭权永远在发送侧。
- 忘记关闭 jobs:worker 的 for range 永不退出,WaitGroup 等不到头,程序挂死。
- 主流程提前 return 不再收 results:worker 阻塞在发送上,goroutine 泄漏,只有退出出口齐全才安全。
- 认为加缓冲就等于不阻塞:缓冲只延后阻塞,容量满了照样堵,协调逻辑不能依赖缓冲大小。
检查与复盘
完成下方检查题。自查清单:
- 每个 channel 都能答出「谁发送、谁接收、谁关闭」
- 可能阻塞的发送/接收都有 ctx.Done() 逃逸出口
- 用 cancel 加 WaitGroup 验证过所有 goroutine 能在有限时间退出
参考资料
- Go Spec:channel 类型 —— 发送/接收/关闭的语义权威
- Go 博客:Pipelines and cancellation —— 本课结构的官方完整版
- 标准库 sync —— WaitGroup 与 Once
读完这一课了?
标记完成后,工作台会安排接下来的学习。