
Go里没有内置Future,但用chan模拟最直接
Go不提供Future或Promise类型,官方推荐用chan配合goroutine实现异步结果获取。这不是权宜之计,而是契合Go“通过通信共享内存”的设计哲学。
常见错误是把chan当成“可取消的返回值”来用:比如开一个chan interface{},塞进结果或error,但没考虑关闭、阻塞、泄漏。结果要么select永远等不到,要么recv panic。
- 始终用带类型的通道,比如
chan result,而非chan interface{},避免运行时类型断言失败 - 结果结构体必须包含
error字段:type Result struct { Data string Err error } - 启动goroutine后,务必在完成时关闭通道(或用
defer close(ch)),否则接收方可能永久阻塞
错误不能只靠return err,得随结果一起传出来
异步函数里return err毫无意义——调用方早已退出。错误必须和数据走同一条路,也就是通过chan发出去。有人试图用全局变量或闭包捕获err,这在并发下完全不可靠。
典型误用:go func() { if err := doWork(); err != nil { lastErr = err } }() —— lastErr竞争读写,且无法同步通知调用方。
立即学习“go语言免费学习笔记(深入)”;
- 错误必须和业务数据绑定在同一个结构体里,一起发送到通道
- 不要在goroutine里
log.Fatal或panic,会杀死整个goroutine,但主流程无感知 - 如果上游需要区分“操作失败”和“通道关闭”,就别用
close(ch)表示成功结束,改用nil错误+有效数据,或单独加个done chan struct{}
select超时 + context取消是标配,但顺序不能错
单纯用time.After做超时,会泄露timer;只用context.WithTimeout但没在goroutine里监听ctx.Done(),等于没取消。两者要配合,且监听顺序影响行为。
错误写法:select { case r := —— 每次都新建timer,且无法响应外部取消信号。
- 优先用
ctx.Done()作为第一个case,确保能及时响应取消 - 超时应由
context.WithTimeout统一管理,而不是手写time.After - goroutine内部必须检查
ctx.Err() != nil并提前退出,否则即使上下文取消,后台任务仍在跑 - 通道接收前加
default不是好主意——它会让“等待结果”变成“轮询”,消耗CPU且无法阻塞等待
多个异步任务聚合时,sync.WaitGroup不如errgroup.Group
用sync.WaitGroup等所有goroutine结束,但错误只能靠额外变量收集,极易竞态。更糟的是,一个任务出错,其他还在跑,浪费资源。
errgroup.Group(来自golang.org/x/sync/errgroup)天然支持“任一出错即停止”和错误聚合,且底层用context协调取消。
- 初始化用
eg, ctx := errgroup.WithContext(parentCtx),后续所有goroutine都用这个ctx - 每个任务用
eg.Go(func() error { ... })注册,返回error会被自动收集 - 调用
eg.Wait()会阻塞直到全部完成或首个错误返回,返回第一个非nil错误 - 注意:如果某个任务返回
nil错误但实际没做完(比如忘了return),Wait()仍会认为它成功了
真正难的不是写对单个异步调用,而是在多层嵌套、带重试、需日志追踪、还要对接trace ID的场景里,让错误既不丢失也不重复,还能准确定位是哪个环节挂了。这时候通道结构体里的error字段、ctx.Value里的traceID、以及每层defer里加的recover位置,差一点就会让问题排查变成猜谜。










