Go database/sql 查询超时的 context 传递方式
来源:17golang原创
时间:2026-10-15 08:35:36 344浏览 收藏
Go 里给数据库查询设置超时,关键不是在 SQL 字符串外面再包一层定时器,而是从请求的 context.Context 派生一个带期限的 context,再把它传给 db.QueryContext。查询成功后立即登记 rows.Close,返回时检查 rows.Err;查询失败则根据 context.DeadlineExceeded、context.Canceled 和驱动错误判断原因。
官方资料:https://go.dev/doc/database/cancel-operations
API 参考:https://pkg.go.dev/database/sql
把 context 传到真正执行 SQL 的 QueryContext 才能让取消信号进入 database/sql;WithTimeout 只负责产生期限,不能替代 QueryContext。Rows 由当前函数消费时要在查询成功后关闭,底层驱动是否能及时中断还取决于它对 context 的支持。
先划清请求、超时和连接池边界
一个 HTTP 请求通常已经有自己的 context。查询函数应该接收这个 context,而不是在内部偷偷使用 context.Background()。在函数内部调用 context.WithTimeout 只增加一个更短的查询期限:父 context 先取消,子 context 会跟着取消;查询期限先到,子 context 会先返回超时。
*sql.DB 管理的是数据库连接池句柄,不等于一次查询独占一条连接。QueryContext 负责把 context 带到查询调用,*sql.Rows 则代表此次查询返回的结果集。两类资源责任不要混在一起:查询函数不应因为某次超时就关闭全局 db,但必须释放自己取得的 Rows。

把带超时的 context 传给 QueryContext
查询函数只保留一个 context 入口。下面的示例用查询函数自己的超时时间覆盖“查询最多等待多久”,但仍然保留上游请求取消信号。SQL 使用占位参数,避免把外部输入拼进字符串。
package account
import (
"context"
"database/sql"
"errors"
"fmt"
)
type Account struct {
ID int64
Name string
Status string
}
func FindActiveAccounts(ctx context.Context, db *sql.DB, ownerID int64) ([]Account, error) {
// 查询期限只约束这次数据库操作,父 context 取消时它也会立即收到信号。
queryCtx, cancel := context.WithTimeout(ctx, 3_000_000_000)
defer cancel()
rows, err := db.QueryContext(queryCtx,
"SELECT id, name, status FROM accounts WHERE owner_id = ? AND status = ?",
ownerID, "active",
)
if err != nil {
// 先区分 context 原因,再把其他错误交给上层记录和处理。
if errors.Is(queryCtx.Err(), context.DeadlineExceeded) {
return nil, fmt.Errorf("account query timeout: %w", context.DeadlineExceeded)
}
if errors.Is(queryCtx.Err(), context.Canceled) {
return nil, fmt.Errorf("account query canceled: %w", context.Canceled)
}
return nil, err
}
// QueryContext 成功后才有 Rows,登记关闭责任以覆盖提前返回路径。
defer rows.Close()
accounts := make([]Account, 0, 8)
for rows.Next() {
var item Account
if err := rows.Scan(&item.ID, &item.Name, &item.Status); err != nil {
return nil, err
}
accounts = append(accounts, item)
}
// Next 结束不代表查询一定成功,必须读取 Rows.Err 的尾部错误。
if err := rows.Err(); err != nil {
return nil, err
}
return accounts, nil
}
示例中的 3_000_000_000 是一个明确的纳秒值,真实项目更适合写成 3*time.Second 并引入 time 包,以免阅读者误把数字当成毫秒。这里刻意把错误判断放在 QueryContext 返回错误的分支里:只有查询调用返回错误时,才进入这段原因判断。
查询成功后为什么要立刻关闭 Rows
QueryContext 返回成功,说明结果集对象已经拿到,但不代表所有行都已经读完。函数可能在第一行满足条件后提前返回,也可能在 Scan 出错时直接退出。把 defer rows.Close() 放在成功返回之后,可以覆盖这些路径。
完整遍历到末尾时,标准库可能自动关闭结果集,但不要把“读到了末尾”当成所有分支都安全的理由。提前返回、扫描失败和上层取消都应该由明确的资源责任兜底。若接口把 Rows 返回给调用方,关闭责任就随接口转移,不要在当前函数里提前关闭。
func FindFirstAccount(ctx context.Context, db *sql.DB, ownerID int64) (Account, error) {
queryCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
rows, err := db.QueryContext(queryCtx,
"SELECT id, name, status FROM accounts WHERE owner_id = ? ORDER BY id",
ownerID,
)
if err != nil {
return Account{}, err
}
// 只取第一行也要关闭 Rows,避免提前返回留下结果集资源。
defer rows.Close()
if !rows.Next() {
// Rows.Err 能区分“没有行”和遍历过程中的数据库错误。
if err := rows.Err(); err != nil {
return Account{}, err
}
return Account{}, sql.ErrNoRows
}
var item Account
if err := rows.Scan(&item.ID, &item.Name, &item.Status); err != nil {
return Account{}, err
}
return item, nil
}
这段代码使用了 time.Second,因此完整示例需要同时引入 time。两个示例的重点都是资源边界:cancel 释放派生 context 的计时器资源,rows.Close 释放查询结果集资源,二者不能互相代替。
超时、请求取消和驱动错误怎样区分
判断原因时优先看 context 的状态。到达 WithTimeout 期限通常对应 context.DeadlineExceeded;上游客户端断开、请求被取消或父 context 主动取消通常对应 context.Canceled。如果 context 仍然没有原因,返回值可能是驱动自身的网络、协议或 SQL 错误。
func queryErrorKind(ctx context.Context, err error) string {
if err == nil {
return "ok"
}
// 使用 errors.Is 兼容驱动对 context 错误的包装,而不是比较字符串。
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return "timeout"
}
if errors.Is(ctx.Err(), context.Canceled) {
return "canceled"
}
// 未被 context 解释的错误应保留原错误,交给日志或上层策略分类。
return "driver-or-sql-error"
}
不要只用错误字符串包含“timeout”来决定重试。超时发生在查询可能已经触及数据库的一刻,是否重试要由操作是否幂等、请求剩余期限和业务成本共同决定。对于只读查询,可以在上层按策略重试;对于写操作,不能因为一次超时就假定数据库没有执行。

驱动不支持取消时,超时并不等于立即中断
database/sql 可以接收带 context 的查询,但真正的数据库驱动需要遵守取消语义。官方驱动接口说明指出,支持 context 的查询应在 context 被取消后返回;不支持 context 取消的驱动可能要等数据库查询完成后才返回。因此,调用方可以设置明确的期限,却不能仅凭函数签名保证所有驱动都能立刻终止服务器端工作。
工程上应把这条边界写进数据库接入层:记录查询开始和结束、把超时原因与驱动错误分开、为慢查询设置数据库侧的治理策略,并在连接池指标中观察等待与使用情况。不要用关闭全局 *sql.DB 的方式“强行回收”一条超时查询,这会影响其他请求。
上线前按资源责任做一次复查
| 复查项 | 正确做法 | 容易出现的误区 |
|---|---|---|
| context 来源 | 接收上游 context,再用 WithTimeout 派生 | 查询函数内部使用 Background 丢失取消信号 |
| 查询调用 | 将派生 context 传给 QueryContext | 创建了超时 context,却继续调用 Query |
| 结果集 | 成功返回 Rows 后立即 defer Close,并检查 Rows.Err | 只在读完全部数据后才认为资源安全 |
| 错误分类 | 用 errors.Is 检查 DeadlineExceeded 和 Canceled | 只按错误字符串决定是否重试 |
| 连接池 | 让 database/sql 与驱动处理单次资源归还 | 查询超时就关闭全局 *sql.DB |
为什么设置了 WithTimeout,查询还是没有马上返回?先确认这个 context 确实传给了 QueryContext,再查看驱动是否支持 context 取消;不支持的驱动可能会等底层查询完成。
QueryContext 失败时需要调用 Rows.Close 吗?失败时通常没有可用的 Rows,因此应先处理错误;只有成功拿到 Rows 后才登记 Close。
rows.Err 和 queryErr 有什么区别?queryErr 反映 QueryContext 建立查询时的错误,rows.Err 反映遍历或读取结果集过程中出现的错误,两者都应检查。
超时后能不能直接重试?不能一概而论。只读且幂等的查询可以由上层按剩余期限决定是否重试;写操作要先确认幂等键、事务和服务端执行状态。
把 context 贯穿到 QueryContext、把 cancel 和 Rows.Close 放在各自资源创建之后,再按 context 原因和 Rows.Err 做错误归类,才能同时照顾查询时限与连接池稳定性。
-
860 收藏
-
843 收藏
-
826 收藏
-
809 收藏
-
792 收藏
-
183 收藏
-
377 收藏
-
374 收藏
-
395 收藏
-
300 收藏
-
119 收藏
-
121 收藏
-
239 收藏
-
488 收藏
-
182 收藏
-
476 收藏
-
284 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习