Go的database/sql中Scan不能自动映射struct,必须按SELECT字段顺序手动传入各字段地址;推荐用sqlx通过db tag实现字段名匹配,但需注意NULL处理、别名一致性及列数严格对应。

Scan 不能直接映射 struct,必须手动按字段顺序赋值
Go 的 database/sql 原生不支持自动将查询结果映射到 struct,Scan 只接受变量地址列表,且顺序必须和 SELECT 字段完全一致。很多人误以为传入结构体指针就能自动绑定,结果 panic 或数据错位。
常见错误现象:sql: expected 3 destination arguments, got 1 或字段值被写到错误字段上。
- 必须用
&user.ID, &user.Name, &user.Email这种显式取址方式 - SELECT 字段顺序要和 struct 字段声明顺序(或 Scan 参数顺序)严格对齐;别依赖数据库表字段顺序
- 如果 SQL 用了别名(如
SELECT name AS full_name),struct 字段名无关紧要,只看 SELECT 出来的列序 - NULL 值需用
sql.NullString等类型接收,否则会报sql: Scan error on column index X
用 sqlx.Select 替代原生 Scan 实现字段名自动匹配
想按字段名(而非顺序)映射,推荐用 sqlx 库 —— 它是 database/sql 的轻量增强,兼容所有驱动,且默认开启字段名反射匹配。
使用前提:struct 字段要有导出的、带 db tag 的名字,例如:
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
type User struct {
ID int `db:"id"`
Name string `db:"name"`
Email string `db:"email"`
}然后直接调用:
err := db.Select(&users, "SELECT id, name, email FROM users WHERE active = ?", true)err := db.Get(&user, "SELECT id, name, email FROM users WHERE id = ?", 123)- 字段名不区分大小写,但建议保持 SQL 别名与 tag 一致(如
SELECT user_name AS name配db:"name") - 注意:若 SQL 返回重复字段名(如 JOIN 多表且未加别名),
sqlx会报错,必须用别名区分
Scan 时字段数不匹配的典型坑:COUNT(*) 和 * 混用
写查询时容易忽略返回列数变化,导致 Scan 参数数量对不上。比如本该查单个字段却写了 SELECT *,或反过来在 COUNT 查询里传了多个地址。
-
SELECT COUNT(*) FROM users→ 只能传一个&count(int64类型) -
SELECT id, name FROM users LIMIT 1→ 必须传两个地址:&id, &name,少一个就 panic - 用
rows.Columns()动态获取列名可用于调试,但生产环境别靠它做映射逻辑(性能差、不可靠) - 预编译语句(
Prepare)不影响 Scan 要求,仍需严格匹配列数
嵌套 struct 或 map 映射?别用 Scan,换 sqlx 或自己 Decode
Scan 本身不支持嵌套结构(如 User{Profile: Profile{Age: 0}}),也不支持 map[string]interface{} 直接接收。强行用 interface{} 接收只会得到 []interface{},还得自己遍历转换。
- 简单场景:用
sqlx.DB.Select(&[]map[string]interface{}, query)可以拿到 map 切片,但 lose type safety - 复杂嵌套(如 user + orders + items):不要试图用单条 SQL + Scan 搞定,应拆成主查 + 关联查,或用
sqlc/ent等代码生成工具 - 如果坚持手写,可先
rows.Scan到[]interface{},再用sqlx.Unsafe或自定义反射函数转 struct —— 但字段类型转换易出错,尤其时间、JSON、NULL
实际项目里最常被忽略的是 NULL 处理和字段顺序一致性。哪怕用了 sqlx,一旦 SQL 改了 SELECT 列而没同步更新 struct tag 或字段类型,运行时才暴露问题。上线前务必用真实 NULL 数据跑一遍查询路径。

















