goroutine 中 panic 不会传播到主线程,但若未用 defer+recover 捕获,会导致协程静默退出且无日志;必须在每个 goroutine 入口显式添加 defer func(){recover()}() 才能兜底捕获并记录错误。

goroutine 直接起任务为什么总丢 panic?
因为 Go 的 panic 只影响当前 goroutine,主线程完全感知不到。你写 go func() { panic("boom") }(),程序不会崩溃,但这个 goroutine 就静默死了,错误日志都没有。
真正能用的兜底方式,是在每个 worker 入口加 defer-recover:
func worker(taskCh <-chan Task) {
defer func() {
if r := recover(); r != nil {
log.Printf("worker panic: %v", r)
}
}()
for task := range taskCh {
task.Run()
}
}- 必须放在 goroutine 内部,不能只在 main 里 recover —— 那没用
- recover 只在 defer 函数里有效,且只能捕获本 goroutine 的 panic
- 别只打印日志,建议同时上报监控(比如发到 Sentry 或 Prometheus Counter)
如何让异步任务可取消、可超时、可等待结果?
靠裸 go 不行,得用 context + channel 组合。核心是:任务执行函数接收 ctx context.Context,并在关键阻塞点检查 ctx.Done()。
示例:一个带取消和超时的邮件发送任务
立即学习“go语言免费学习笔记(深入)”;
func sendEmail(ctx context.Context, to, subject string) error {
// 用 WithTimeout 包一层,避免外部 ctx 超时太短
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
<pre class='brush:php;toolbar:false;'>select {
case <-time.After(8 * time.Second): // 模拟发信
return nil
case <-ctx.Done():
return ctx.Err() // 返回 context.Canceled 或 context.DeadlineExceeded
}}
- 不要在 goroutine 里直接传
context.Background()—— 失去控制权 - 所有 I/O 操作(
http.Client.Do、db.QueryContext)都优先用带 Context 的变体 - 如果任务要返回结果,用
chan Result而不是 void,否则调用方永远不知道成没成
channel 任务队列缓冲区设多大才不丢任务也不卡死?
缓冲大小不是拍脑袋定的 100 或 1000,而是按「峰值 QPS × 平均处理耗时」算出来的。比如每秒最多来 200 个任务,每个平均花 50ms 处理,那缓冲至少要 200 × 0.05 = 10,再乘个 2~3 倍安全系数,设成 30~50 就够用。
提交任务时也别直接 taskCh ,得防阻塞:
select {
case taskCh <- t:
default:
log.Warn("task queue full, dropped")
// 或者走降级:同步执行、存 DB 待重试、发告警
}- 不带缓冲的
chan Task在高并发下会立刻阻塞生产者,HTTP handler 就卡住 - 缓冲太大(比如 10000)等于把压力从内存转移到 GC 和调度器,反而更容易 OOM
- worker 数量别跟
runtime.NumCPU()绑定 —— I/O 密集型任务 3~5 个足够,CPU 密集型才考虑更多
重试逻辑该放在任务内部还是统一中间件?
放任务内部更可控。Go 没有 Java 那种统一的 exceptionally 链式回调机制,每个任务的失败语义不同:网络超时要重试,参数校验失败就该直接拒掉。
推荐写一个通用重试封装,但由调用方决定是否启用:
func withRetry(fn func() error, max int, backoff time.Duration) error {
var err error
for i := 0; i <= max; i++ {
err = fn()
if err == nil {
return nil
}
if !isRetryable(err) { // 自定义判断,比如只重试 net.Error
return err
}
if i < max {
time.Sleep(backoff)
backoff *= 2 // 指数退避
}
}
return err
}-
isRetryable必须自己实现,标准库不提供——errors.Is(err, context.Canceled)这类就不能重试 - 重试次数别硬编码,最好从配置或 context.Value 里取,方便线上动态调
- 别在重试里埋 log.Info,容易刷爆日志;用 log.Debug + counter 上报更实用
最常被忽略的一点:recover 后如果不清空 channel 缓冲或没 close taskCh,整个 worker 池可能卡在 for range 里永远等不到退出信号。


















