正确做法是select配合time.After并列等待:resultCh := make(chan string, 1);go func() { resultCh

用 select + time.After 实现单次任务超时
Go 里没法强制杀掉一个正在跑的 goroutine,所以“超时就停函数”是伪需求;真正能做的是“超时就放弃等待结果”。最常用、最轻量的方式就是 select 配合 time.After。
常见错误是把 time.After 放在 select 外面反复调用,或者漏掉接收方导致 channel 阻塞。正确写法是让两个 case 并列等待:
resultCh := make(chan string, 1)
go func() {
resultCh <- doSomething() // 耗时操作
}()
<p>select {
case res := <-resultCh:
fmt.Println("成功:", res)
case <-time.After(2 * time.Second):
fmt.Println("超时,不等了")
}-
resultCh必须带缓冲(至少 1),否则 goroutine 可能卡在发送上,主流程永远收不到结果 -
time.After返回的是,不能重复读,每次超时判断都得新建一个 - 这个模式只适用于“一次执行、一次判断”,不适合长周期轮询或重试场景
用 context.WithTimeout 控制带取消信号的任务
当任务本身支持中断(比如 HTTP 请求、数据库查询、文件读取),必须用 context.Context 才能真正释放资源。单纯靠 time.After 只是不等了,但后台 goroutine 还在跑,可能浪费 CPU 或占用连接。
典型误用是创建了 ctx 却没传给下游函数,或者忘了调用 cancel():
立即学习“go语言免费学习笔记(深入)”;
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel() // 必须 defer,否则可能泄漏
<p>resultCh := make(chan string, 1)
go func() {
resultCh <- doSomethingWithContext(ctx) // 函数内部需检查 ctx.Done()
}()</p><p>select {
case res := <-resultCh:
fmt.Println("完成:", res)
case <-ctx.Done():
fmt.Println("被上下文取消:", ctx.Err()) // 可能是 timeout 或 manual cancel
}- 所有标准库 I/O 操作(
http.Client.Do、sql.DB.QueryContext)都接受context.Context - 自定义函数若要响应取消,必须在关键阻塞点轮询
select { case - 不调用
cancel()会导致 timer 泄漏,尤其在高频调用场景下内存缓慢增长
给 sync.WaitGroup.Wait() 加超时要小心 goroutine 泄漏
WaitGroup 本身不提供超时,但封装一层很容易。最容易踩的坑是:goroutine 启动后没关闭 channel,或者 WaitGroup 传值而非传指针。
正确实现的关键点:
func WaitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
ch := make(chan struct{})
go func() {
defer close(ch) // 确保无论 panic 还是正常结束,channel 都会关
wg.Wait()
}()
select {
case <-ch:
return false
case <-time.After(timeout):
return true
}
}- 必须传
*sync.WaitGroup,否则 goroutine 里调wg.Wait()等的是副本,永远等不到 - 用
defer close(ch)比ch 更安全,覆盖 panic 场景 - 如果超时返回
true,原WaitGroup仍在运行,后续仍需保证它最终完成,否则可能引发逻辑错乱
并发任务批量超时控制别硬扛 goroutine 数量
一次性起几百个 goroutine 做超时控制,看着简单,实际容易触发调度瓶颈和内存压力。尤其当每个任务都要开自己的 time.After 和 channel,资源开销翻倍。
更合理的做法是用固定 worker 池 + 单个全局超时 channel,或直接用成熟库(如 ants):
- 自己控并发:用带缓冲的限流 channel(如
sem := make(chan struct{}, 10))控制同时最多 10 个任务在跑 - 避免每个任务都配独立 timer:对整批任务设总超时,而不是每个子任务单独计时
- 真实项目中,优先考虑
ants.SubmitWithTimeout(task, 3*time.Second)这类封装,省去手动管理 goroutine 生命周期的麻烦
超时逻辑本身不难,难的是边界情况——比如 channel 未关闭、context 未传递、WaitGroup 传值、timer 泄漏。这些地方不出问题则已,一出就是线上偶发卡顿或内存持续上涨。


















