go用channel和goroutine替代回调,避免回调地狱;每个异步任务配专属channel只写一次结果,主逻辑用

Go里没有传统回调函数,用channel和goroutine协作更自然
Go语言不鼓励也不需要像JavaScript或Node.js那样写嵌套回调函数来处理异步逻辑。它的并发模型天然规避了“回调地狱”——goroutine启动轻量、channel传递结果,两者配合就能清晰表达“等一个异步操作完成后再做某事”。你写的不是回调,而是同步风格的代码,背后由调度器自动协调。
用channel接收异步结果,避免手动管理状态
常见错误是把channel当成“事件总线”反复写入,或者忘记关闭导致接收方永久阻塞。正确做法是每个异步任务配一个专属chan,只写一次结果,接收方用直接取值。
- 启动异步任务时,传入一个
chan Result,任务结束时写入ch - 主逻辑用
result := 等待,这行代码会阻塞直到有值,但不会卡死整个程序(因为goroutine独立运行) - 不要用
for range ch监听单次结果;除非明确要收多个值,否则直接即可 - 如果可能失败,建议用
chan struct{ data T; err error }或分开两个chan,避免零值歧义
select配合timeout和default,防止goroutine泄漏
纯可能永远等不到值,尤其在超时、取消或上游panic时。必须用<code>select控制等待行为,否则goroutine会堆积。
-
select里加case 实现超时 - 加
default分支可做非阻塞尝试(但慎用,容易忙轮询) - 若使用
context.Context,应监听ctx.Done()并在退出前关闭相关channel - 注意:
select中多个case就绪时是随机选一个,不能依赖顺序
替代回调的真实场景:HTTP请求+解析+入库
比如发HTTP请求后解析JSON再存数据库,不用写三层嵌套回调。实际代码结构通常是:
func doWorkflow(ctx context.Context) error {
ch := make(chan result, 1)
go func() {
defer close(ch)
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
if err != nil {
ch select {
case r := <-ch:
if r.err != nil {
return r.err
}
return saveToDB(r.data)
case <-ctx.Done():
return ctx.Err()
}
}
关键点在于:所有异步步骤被封装进一个goroutine,结果统一走ch;主流程保持线性,错误和超时由select统一捕获。没人去注册回调函数,也没有层层传递callback参数。
真正容易被忽略的是close(ch)时机和context传播——漏掉前者会导致接收方无法判断是否已结束,漏掉后者会让超时或取消信号无法穿透到goroutine内部。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











