前言
最近,一个数据查询服务被业务方反馈拿不到数据,但接口响应是成功的,不报错,仔细排查后发现数据查询库用的是 pgx,但 pgx 返回的错误未被处理,导致服务接口没有响应错误。
在后续的排查过程中,发现这其实不算是 pgx 的问题,而是 database/sql 中的坑,所有涉及用 database/sql 查询的都需要显式处理 rows.Err()。
问题篇
服务所用 pgx 版本为 4.10.1。查询函数主要用的是 QueryRow(返回一条数据) 和 Query(返回多条数据),更近一步的测试中(人为制造查询错误,eg:锁表)发现,调用 QueryRow 函数的接口,如果发生查询错误的问题,服务接口会正常响应错误。深入 pgx 源码发现,QueryRow 本质是对 Query 的进一步封装,对应的 Scan 函数源码为:
1 |
|
从源码中可看出 QueryRow 的 Scan 函数有一系列的错误处理,而 Query 对应的 Scan 是更底层的函数,返回的仅是 Scan 过程中的错误,其他的错误需要在业务上层处理。Close 函数同样可能会出现错误,需要调用 rows.Err() 主动检查错误(这一步至关重要)。对于 Close 报的错,可以这样处理:
1 | var err error |
数据库执行 sql 失败的错误(eg:canceling statement due to conflict with recovery),在 Close 后才会暴露出来,所以不处理这个错误,就不会返回错误,但数据又查不到,服务接口也表现为响应成功,导致上层业务误认为数据库里还真没数据。
最好的方式还是避免每次都手动 Scan,pgx 其实还提供了更上层的函数 QueryFunc,该函数封装了大部分错误处理:
1 | func (c *Conn) QueryFunc(ctx context.Context, sql string, args []interface{}, scans []interface{}, f func(QueryFuncRow) error) (pgconn.CommandTag, error) { |
不过 QueryFunc 函数在新版本中(5.7.2)已被 ForEachRow 替代:
1 | func ForEachRow(rows Rows, scans []any, fn func() error) (pgconn.CommandTag, error) { |
而 ForEachRow 的使用示例可以看这个函数:
1 | func (c *Conn) getCompositeFields(ctx context.Context, oid uint32) ([]pgtype.CompositeCodecField, error) { |
后记
对于一个不熟悉的底层库,最好的学习方式还是看它的示例代码,库的开发者很难知道用户会踩哪些坑,文档中自然不会有,毕竟当局者迷。只从文档出发,很容易陷进未知的坑里,甚至掉坑里都不知道,业务出问题后,花费大代价排查之后,才知道掉坑里了。陌生的开源库在使用的时候还是先全库 clone 下来,用 api 的时候,就去源码里搜一下,看看开发者写的示例(不管是测试,还是其他地方的调用),当然现在也可以让 AI 先写,人只要再核实一下文档和源码,能节省很多学习的功夫。