因为每次循环都新建 timer,倒计时重置导致超时永不触发;正确做法是将 time.After 移到循环外,复用同一 channel。

为什么 time.After 放在循环里就失效
超时没触发?大概率是把 time.After 写进了 for 循环体内部。每次调用 time.After(1 * time.Second) 都会新建一个独立的 timer 和 channel,倒计时从头开始算——相当于每轮 select 都重置 1 秒钟,总耗时再长也不会超时。
- 错误写法:
for _, f := range funcs { select { case res := —— 每次都新启一个 1 秒计时器 - 正确做法:把
timeout := time.After(1 * time.Second)提到循环外,所有 select 共享同一个通道 - 注意:这只能控制“等待结果”的超时,不会中断正在跑的 goroutine;InsertionSort 跑了 7.6 秒,超时后它还在后台执行
要用 context.WithTimeout 而不是手拼 select
自己写 select + time.After 容易漏掉 cancel、复用逻辑、错误分类,而 context.WithTimeout 把定时、取消、错误类型(context.DeadlineExceeded vs context.Canceled)全包圆了。
- 必须成对使用:
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second),且defer cancel()不可省略,否则底层 timer 不释放 - goroutine 内部要主动检查:
select { case ,不能只等一次就完事 - 标准库组件(如
http.NewRequestWithContext、db.QueryContext)能直接响应 ctx,比手动包装更可靠
并发任务顺序 ≠ 启动顺序,靠 channel 写入时机保证
Go 不保证 goroutine 执行完成顺序,但你可以控制「结果进入 channel 的顺序」。关键不是谁先 go,而是谁先 ch 。
- 每个 goroutine 应该处理完再写 channel,避免边算边发导致乱序
- 如果需要严格按
sortingFunctions切片索引顺序输出,别依赖 for-range 接收顺序,改用带索引的结构体:ch - 闭包捕获循环变量是常见坑:
for i := range funcs { go func() { fmt.Println(i) }() }会全打 3;应写成go func(idx int) { ... }(i)
CPU 密集型任务超时必须手动插入检查点
context.Context 对纯计算无感——它不抢占,只协作。如果函数里全是 for 循环加加减减,不碰 channel、I/O 或 time.Sleep,那 ctx.Done() 永远不会被监听到。
立即学习“go语言免费学习笔记(深入)”;
- 必须在循环体内定期检查:
if ctx.Err() != nil { return ctx.Err() },比select更轻量 - 避免在紧循环里高频
select,容易忙等;优先用ctx.Err() != nil快速判断 - 没有“强制杀 goroutine”机制,硬中断会导致资源泄漏或 panic,所以检查点得你自己加
超时和顺序这两件事,本质都是对 channel 生命周期和写入时机的控制;真正难的不是语法,而是想清楚“我要等的是什么”——是等结果,还是等它停;是等全部完成,还是只要第一个有效结果。


















