必须使用 context.WithTimeout 而非 time.AfterFunc 或 select 轮询,因 database/sql 的 QueryContext 等方法才真正响应取消;手动轮询仅提前返回错误,不中断 SQL 执行,导致资源泄漏;超时应由调用方注入,避免内部新建 context.Background()。

超时控制必须用 context.WithTimeout,不是 time.AfterFunc 或 select 手动轮询
Go 的数据库驱动(如 database/sql)原生支持 context.Context,超时必须通过 context.WithTimeout 注入,否则查询可能卡死、goroutine 泄漏。手动用 time.After + select 不会中断底层 SQL 执行,只是提前返回错误,连接和语句资源仍被占用。
-
db.QueryContext、db.ExecContext、stmt.QueryContext等才是真正响应 context 取消的入口 - 不要在函数内部新建
context.Background()再套 timeout——这会让调用方无法传递 cancel 信号 - 超时时间建议从配置读取,但需做校验:
if timeout
配置项应支持毫秒级精度,且默认值要显式声明
YAML/JSON 配置里写 "timeout_ms": 3000 比写 "timeout": "3s" 更易解析和校验,也避免字符串解析失败导致 panic。Go 标准库不直接支持 duration 字符串反序列化到 struct,容易出错。
- 定义配置结构体时用
int64存毫秒值,运行时转成time.Duration:time.Duration(cfg.TimeoutMS) * time.Millisecond - 必须设默认值,比如
TimeoutMS: 5000,防止配置缺失时传入 0 导致无限等待 - 如果用 viper,记得调用
viper.SetDefault("db.query_timeout_ms", 5000),而不是依赖 struct tag 默认值
rows.Err() 必须检查,否则超时错误会被静默吞掉
很多人只检查 rows, err := db.QueryContext(ctx, sql, args...) 的 err,但超时发生时 err 可能为 nil,而后续 rows.Next() 或 rows.Close() 才真正返回 context.DeadlineExceeded。这是最常被忽略的坑。
- 正确模式是:获取
rows后立即 deferrows.Close(),然后循环rows.Next(),并在循环结束后调用rows.Err() - 示例片段:
rows, err := db.QueryContext(ctx, sql, args...) if err != nil { return err } defer rows.Close() for rows.Next() { // scan... } return rows.Err() // ← 这里才捕获超时或网络错误 - 若用
rows.Slice()类封装库,确认其内部是否已调用rows.Err();没确认前别假设它安全
测试超时场景不能只 mock db,得真跑带 cancel 的 case
单元测试里 mock database/sql 接口很容易绕过 context 行为,导致“测了等于没测”。真实验证超时,需要启动一个故意延迟的本地数据库(如 PostgreSQL 的 pg_sleep(10)),或用 sqlmock 显式设置 QueryContext 返回 cancel 错误。
立即学习“go语言免费学习笔记(深入)”;
- 用
sqlmock时,必须调用mock.ExpectQuery().WithContext(...)并指定自定义 context,否则 mock 不触发 cancel 分支 - 集成测试推荐起一个轻量容器(如
docker run -d -p 5432:5432 postgres),执行SELECT pg_sleep(5)配合 1s 超时,看是否真返回context.DeadlineExceeded - 注意:
TestMain中启停 DB 容器要加os.Exit(m.Run()),否则 test 被 timeout 信号 kill 后不会清理资源
超时配置不是加个参数就完事,关键在 context 传递链是否完整、错误是否被最终消费、测试能否击中真实取消路径。少一环,线上就可能积压 goroutine。


















