net/http 默认非异步是因为其并发依赖 goroutine 而非异步 io 抽象,真正异步需手动用 errgroup.group 等工具实现并发控制、错误聚合与上下文取消。

为什么 net/http 默认不是异步的?
Go 的 net/http 服务器本身是并发的(每个请求跑在独立 goroutine),但它不提供「异步 IO 回调」或「非阻塞等待」这类抽象。你写 http.HandleFunc,函数体里调用 db.QueryRow 或 http.Post,整个 goroutine 就卡住等结果——这不是 IO 多路复用意义上的“异步”,只是靠 goroutine 数量堆出来的并发。
真正需要简化异步 IO 的场景,比如:一个 HTTP handler 要同时发 3 个下游 API 请求、等全部返回再聚合;或者要轮询多个设备状态,但不想为每个开 goroutine + channel 手动编排。
- 别指望标准库自动帮你做并发控制或错误聚合,
sync.WaitGroup和errgroup.Group是你绕不开的搭档 -
context.Context必须传到底层调用,否则超时和取消会失效——尤其在嵌套 HTTP 调用或数据库查询时 - 用
goroutine匿名起一堆请求很容易,但没加recover或没设context.WithTimeout,可能让整个 handler 卡死或泄漏 goroutine
用 errgroup.Group 并发发起多个 HTTP 请求
这是最常见也最稳妥的简化方式。它把并发控制、错误传播、上下文取消全包了,比裸写 sync.WaitGroup + channel 清晰得多。
g, ctx := errgroup.WithContext(r.Context())
var resp1, resp2 *http.Response
g.Go(func() error {
var err error
resp1, err = http.DefaultClient.Do(http.NewRequestWithContext(ctx, "GET", "https://api.a.com/data", nil))
return err
})
g.Go(func() error {
var err error
resp2, err = http.DefaultClient.Do(http.NewRequestWithContext(ctx, "GET", "https://api.b.com/status", nil))
return err
})
if err := g.Wait(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 此时 resp1 和 resp2 已就绪,可读 Body
- 必须用
http.NewRequestWithContext,不能用http.Get——后者忽略传入的ctx -
errgroup.Group在任一子任务出错时立刻取消其余任务(通过内部共享的ctx),避免资源浪费 - 注意
resp.Body仍需手动Close(),errgroup不管这个
数据库查询也适用 errgroup 吗?
可以,但得看驱动是否支持 context。比如 database/sql 的 QueryRowContext、ExecContext 都是安全的;而老式 QueryRow 会忽略 timeout。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- PostgreSQL 驱动(
lib/pq)和现代pgx都完整支持 context 取消 - MySQL 驱动(
go-sql-driver/mysql)从 v1.5+ 开始支持QueryContext,旧版本会静默忽略 - 别在
g.Go里直接用tx.QueryRow——事务对象不是并发安全的,应为每个 goroutine 单独BeginTx或改用连接池级并发
典型错误是:用同一个 *sql.DB 发起多个查询没问题,但共享一个 *sql.Tx 就会 panic,报 driver: bad connection 或 transaction is already closed。
要不要引入第三方框架如 gofiber 或 echo?
它们本身不解决异步 IO 简化问题。Fiber 的 c.Async 只是把 handler 函数扔进 goroutine,不处理错误聚合、上下文传播或并发限制——你还是得自己上 errgroup。
- 框架的价值在于路由、中间件、JSON 序列化等,而不是替代
errgroup或context - 有些封装库(如
github.com/sony/gobreaker)能帮你加熔断,但那是另一层关注点,和“并发发起多个 IO”无关 - 真要简化,与其换框架,不如把
errgroup封装成一个带默认超时的工具函数,复用在多个 handler 里
最容易被忽略的是:所有 IO 操作链路上的每一步,都得显式接收并传递 context.Context。漏掉一层,整条链就失去取消能力——比如 HTTP handler 传了 ctx 给 errgroup,但里面调用的某个 SDK 方法没暴露 Context 参数,那就只能干等。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










