不能在 handler 里直接调 db.queryrowcontext,因其会导致 rt 线性累加、goroutine 积压、连接池耗尽,引发雪崩;正确做法是将数据获取下沉至 domain 层或 dataloader,并配合批处理、缓存与合理超时控制。

为什么不能在 handler 里直接调 db.QueryRow
微服务里一次 HTTP 请求常需聚合多个数据源(比如查订单再查用户再查地址),如果每个逻辑都直接在 handler 中调 db.QueryRowContext,RT 会线性累加,goroutine 积压,连接池迅速耗尽——这不是慢,是雪崩前兆。真正卡点不是 SQL 写得不好,而是查询发起的位置和时机错了。
- 数据获取必须下沉到 domain 层或独立的
DataLoader组件,handler 只负责编排、不碰 DB - 同一请求内重复查相同 ID(如 10 个订单查同一个用户 ID=123),必须启用批处理 + 缓存,否则立刻触发 N+1
-
db.QueryRowContext适合单行确定存在场景;若可能为空,务必检查返回的err,否则sql.ErrNoRows被静默吞掉,字段全为零值
怎么用 dataloader 消除 N+1 却不踩坑
go-graph-gophers/dataloader 能把同 batch 内的 key 合并成一条 IN 查询,但只适用于「单表主键等值查询」,对 JOIN、WHERE status = 'paid' AND created_at > NOW() - INTERVAL '7 days' 这类条件完全无效。
- batch 函数必须返回
[]*User,不能返回map[int]*User,否则空值位置错位会导致 panic - 默认 timeout 是 1ms,高并发下易过早合并,建议设为
5 * time.Millisecond,并配合WithWait控制延迟 - 它不解决跨服务聚合问题——比如订单服务要查用户服务的头像,得靠服务间 gRPC 调用 + 该服务内部的 dataloader,不能指望本层 loader 跨库查
什么时候该放弃 ORM,改用 pgx + 手写 SQL
当查询涉及 JSONB 解析、数组操作、全文检索(to_tsvector)、或需要复用 prepared statement 提升吞吐时,gorm 或 sqlc 生成的代码会成为瓶颈。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
-
pgx的QueryRow比database/sql快 30%+,尤其扫描pgtype.JSONB时零拷贝,省掉json.Unmarshal开销 - 用
pgxpool替代sql.DB,连接复用更激进,且支持AcquireConn(ctx)显式控制生命周期 - 别把
pgx当作“高级 database/sql”来用——它的Query返回的是pgx.Rows,直接 Scan 更快,绕过标准库反射路径
读写分离中间件里最危险的误判逻辑
不能靠 SQL 字符串匹配 SELECT 来判断是否走从库。ORM 拼的语句、WITH RECURSIVE、INSERT ... RETURNING 都可能含 SELECT 却必须走主库。
- 事务内所有操作强制走主库,哪怕调了
Query也得透传给masterDB,否则主从延迟导致查不到刚写入的数据 -
SELECT FOR UPDATE、刚INSERT就查同表最新记录、用了LAST_INSERT_ID()等会话变量函数,一律禁止走从库 - 稳妥做法是默认读走从库,但提供显式 API 如
WithMaster().Query(...),让业务代码自己担责
真正的难点不在“怎么写 SQL”,而在于谁持有连接、何时释放、批量边界在哪、主从切换时如何兜底。这些细节一旦漏掉,压测时才暴露,但线上已经雪崩。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










