G10第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

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]

动手任务

  1. 把 workers 从固定值改成 min(len(urls), 8),避免 3 个任务开 8 个 goroutine 的浪费。
  2. 故意删掉 close(jobs) 那行,go run . 观察程序永不结束——亲眼看一次泄漏形态,再改回来。
  3. 给 checkResult 加 Err 字段,把网络错误和状态码错误分开收集,体会结果与失败在类型上的区分。

常见错误

  • 接收方调用 close:panic(close of closed channel)或违反所有权约定,关闭权永远在发送侧。
  • 忘记关闭 jobs:worker 的 for range 永不退出,WaitGroup 等不到头,程序挂死。
  • 主流程提前 return 不再收 results:worker 阻塞在发送上,goroutine 泄漏,只有退出出口齐全才安全。
  • 认为加缓冲就等于不阻塞:缓冲只延后阻塞,容量满了照样堵,协调逻辑不能依赖缓冲大小。

检查与复盘

完成下方检查题。自查清单:

  • 每个 channel 都能答出「谁发送、谁接收、谁关闭」
  • 可能阻塞的发送/接收都有 ctx.Done() 逃逸出口
  • 用 cancel 加 WaitGroup 验证过所有 goroutine 能在有限时间退出

参考资料

检查题共 2 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1读代码较难

阅读代码(Go 1.22+,go run 可直接运行)。运行结果是什么?

package main

import "fmt"

func main() {
	ch := make(chan int, 3)
	ch <- 1
	ch <- 2
	close(ch)
	for v := range ch {
		fmt.Println(v)
	}
	ch <- 3
	fmt.Println("sent")
}
Q2多选简单(多选,全部选对才算正确)

ch 是一个已关闭、缓冲已取空的 chan int。下列说法正确的有哪些?(多选)

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...