反射拼WHERE时nil指针直接调v.Interface()必panic,须先v.Kind()==reflect.Ptr且v.IsNil()判空再处理;结构体+db tag比map更安全,避免类型擦除;命名参数需匹配sqlx.Named或原生驱动,不可混用。

反射拼 WHERE 条件时,nil 指针直接调 v.Interface() 必 panic
这是动态拼 SQL 最常见的崩溃点:结构体字段是 *string、*int64 等指针类型,值为 nil,但反射取值前没做判空,直接调 v.Interface() 就触发 nil pointer dereference。
关键不是看 v.IsValid()——它对 nil 指针返回 true,但后续仍会崩;必须显式判断:
- 先用
v.Kind() == reflect.Ptr识别指针类型 - 再用
v.IsNil()判断是否为空,是则跳过该字段,或生成"col IS NULL" - 若非 nil,需调
v = v.Elem()解引用后继续处理字段值 - 接口类型(如
interface{})要额外注意:可能包装了 nil 指针,得先v.Elem()再判,否则v.Interface()返回空interface{}而非底层值
用结构体 + db tag 比 map[string]interface{} 更安全
虽然 map 看似灵活,但它丢失类型和字段语义:空字符串 "" 和 nil 在 SQL 中含义完全不同(= '' vs IS NULL),而 map 无法区分;且参数绑定时易因类型擦除出错。
结构体配合 db tag 是更可控的选择:
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 定义字段时加 tag,例如
Name *string `db:"user_name"` - 反射时用
v.Tag.Get("db")取列名,不依赖字段名与数据库一致 - 避免在 struct 中混用零值和 nil:比如
CreatedAt *time.Time为 nil 表示“未设置时间”,而非“0001-01-01” - 如果必须用 map,建议封装校验函数统一归一化:
nil → IS NULL,"" → = '',0/zero → = ?
sqlx.Named 和原生 database/sql 对占位符处理不兼容
反射生成的 SQL 字符串里如果含命名参数(如 :name),直接喂给 db.Query 会报错 sql: expected 0 arguments, got 1——因为原生驱动只认位置参数(? 或 $1),不解析命名占位符。
必须在反射阶段就决定参数格式,不能混用:
- 选
sqlx.Named:保持传入 struct 或 map 不变,SQL 中写:field_name,由 sqlx 替换 - 选原生驱动:反射时就得把字段名和值拆成两个切片:
args = append(args, v.Interface()),同时维护placeholders列表(如[]string{"?"}或["$1", "$2"]) - 千万别在 SQL 字符串里硬拼
WHERE name = ?却又用sqlx.Named执行——占位符不会被识别,变成字面量,查不到数据也无报错 - IN 子句用
sqlx.In更省心,但反射中需识别v.Kind() == reflect.Slice并用v.Len()生成对应数量的?
分页部分别用反射动态拼 LIMIT 和 OFFSET,但排序字段必须提前确定
反射适合拼 WHERE,但 LIMIT/OFFSET 值来自 HTTP 参数(page、limit),不应由反射从结构体读取——那是业务逻辑层职责,不是 ORM 映射层的事。
真正容易被忽略的是 ORDER BY:
- 没
ORDER BY的分页结果不可靠,MySQL/PostgreSQL 不保证无序 LIMIT 的稳定性,尤其并发写入时可能漏或重 - 排序字段必须带索引;优先用主键
id ASC或created_at DESC, id DESC(防时间重复) - 别在反射拼 SQL 时动态决定排序字段——应由调用方明确传入,比如
Paginate(db, &users, opts{OrderBy: "id DESC"}) - 游标分页(
WHERE id > ? ORDER BY id)完全绕过 OFFSET,但要求排序字段严格单调,反射层只需确保 WHERE 条件能正确拼出id > ?即可

















