单次查询超时必须用readTimeout和writeTimeout,而非timeout=5s;前者控制SQL执行阶段读写响应超时(如SELECT返回、大事务提交),后者仅管TCP建连,设错将导致慢查询无限hang住。

单次查询超时不能靠 timeout=5s 实现,它只管建连,不管 SQL 执行;必须用 readTimeout 和 writeTimeout。
readTimeout 和 writeTimeout 才是真正控制 SQL 执行阶段的超时
很多人在 DSN 里加了 timeout=5s 就以为查询会自动中断,结果发现慢查询卡住几十秒都没反应——因为那个参数只作用于 net.Dial() 阶段,即 TCP 连接建立过程。一旦连接建好,后续 SELECT/UPDATE 的执行时间完全不受控。
真正起效的是这两个参数:
-
readTimeout=30s:控制从数据库读取响应的最长等待时间(比如SELECT返回结果) -
writeTimeout=60s:控制向数据库发送请求的最长等待时间(比如大事务提交、大批量INSERT)
它们必须显式写进 DSN,GORM 不会默认启用。例如 MySQL DSN 应类似:
user:pass@tcp(127.0.0.1:3306)/db?charset=utf8mb4&parseTime=true&loc=Local&readTimeout=30s&writeTimeout=60s
注意:& 是 URL 编码后的 & 符号,别漏掉;单位必须带 s,写成 30 会被忽略。
为什么只设 readTimeout 不够?writeTimeout 在这些场景会触发
以下情况不走 readTimeout,但可能卡死,必须靠 writeTimeout 拉回来:
- 大事务提交时,数据库正在刷盘或等锁,客户端发不出 COMMIT 命令
- 批量
INSERT数据量极大,客户端写缓冲区满,阻塞在Write()系统调用 - 网络中间件(如云厂商代理、NAT)异常,导致 TCP 包发不出去
如果只设 readTimeout,上述情况会无限 hang 住,直到操作系统 TCP keepalive 触发(通常要 2 小时),远超业务容忍。
超时参数和连接池参数不是一回事,别混着调
有人试图用 SetConnMaxLifetime 或 SetMaxIdleConns 来“间接”限制查询时长,这是无效的。前者控制连接对象存活时间,后者控制空闲连接数量,都不干预单条 SQL 的执行流程。
真正影响查询中断行为的只有:
- DSN 中的
readTimeout/writeTimeout - 应用层是否用
context.WithTimeout包裹 GORM 调用(推荐补一层,双重保险)
例如:
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second) defer cancel() db.WithContext(ctx).First(&user)
这样即使数据库没配 readTimeout,Go 层也能在 25 秒后主动断开。但注意:这不会终止数据库端的 SQL 执行,只是让客户端放弃等待。
最易被忽略的一点:MySQL 服务端本身也有 wait_timeout 和 max_execution_time,如果客户端超时设得比服务端还长,就白配了。上线前务必确认两边对齐。


















