row 和 rows 是 go 标准库 sql.row 与 sql.rows 的透出,绕过 gorm 映射、事务和钩子;适用于大数据量、动态字段、流式处理等场景,但需手动处理错误、关闭连接及类型序列化。

Row 和 Rows 不是 GORM 自己定义的类型,而是直接透出的 *sql.Row 与 *sql.Rows —— 它们来自 Go 标准库 database/sql。这意味着你用它们时,就等于绕过 GORM 的结构体映射、事务封装、钩子等机制,直连底层驱动。选对用法,能解决 Scan 无法覆盖的场景;用错地方,轻则资源泄漏,重则连接池耗尽。
什么时候必须用 Rows 而不是 Scan
当你需要处理「结果集过大」或「字段动态不确定」的查询时,Scan 会一次性把全部数据加载进内存并尝试结构体绑定,极易 OOM;而 Rows 是游标式读取,逐行解析,可控性强。
- 查百万级日志表,只取
id和created_at做归档:用Rows配合rows.Scan(&id, &ts),避免构造百万个结构体 - 执行
SELECT *但表结构可能随版本变化(如多租户系统):用rows.Columns()动态获取字段名,再用rows.Scan绑定[]interface{}切片 - 需要在遍历中做条件跳过、提前中断、或异步写入(如流式导出 CSV):
Rows提供Next()控制权,Scan没有中间态
Row 的常见误用:单行查询也得手动检查 Err()
db.Table("users").Where("id = ?", 1).Row() 返回的是 *sql.Row,它不自动报错 —— 即使数据库返回空结果、类型不匹配、甚至网络中断,row.Scan() 都可能静默成功或 panic。必须显式检查错误:
-
err := row.Scan(&name, &email)后,要判断err == sql.ErrNoRows还是其他真实错误 - 即使确认只有一行,也要调用
row.Err():因为Scan只校验本次解包,而row.Err()才反映查询全过程是否出错(比如 WHERE 条件导致无匹配,但驱动未返回 ErrNoRows) - 别省略
defer rows.Close()——Row不需要,但Rows必须;很多人混淆两者,给Row加defer无害,但给Rows忘加就会泄漏连接
Rows 与 GORM Scan 混用的风险点
不要在一个查询里先调 db.Raw(...).Rows(),再试图拿同一个 *gorm.DB 实例去 .Scan() 同一 SQL —— 它们走的是不同执行路径,事务上下文、预编译状态、甚至连接可能不一致。
-
Rows默认不参与 GORM 的事务管理:如果你在tx := db.Begin()中调tx.Raw(...).Rows(),该Rows仍可能从连接池取新连接,导致读不到未提交数据 -
Scan会自动关闭底层*sql.Rows,但Rows()返回的*sql.Rows不会;若你误用db.Raw(...).Rows().Scan(...)(语法错误),编译都过不了 ——*sql.Rows没有Scan方法,只有Scan函数接收它 - 字段顺序必须严格匹配:GORM
Scan依赖结构体标签或变量顺序,而Rows.Scan()完全按 SQL SELECT 字段顺序来,错一位就 panic 或数据错位
最易被忽略的一点:无论用 Row 还是 Rows,只要涉及 time.Time、json.RawMessage、自定义 Valuer/Scanner 类型,你就得自己处理序列化逻辑 —— GORM 的那一套标签和钩子,在这里全部失效。











