反射不能直接拼出安全SQL字符串,必须生成columns和args切片供预编译语句使用;字段映射需遍历导出字段并解析db标签,扫描时须避免循环变量地址陷阱。

反射不能直接拼出安全 SQL 字符串
直接用 reflect.Value.Interface() 拼接 SQL 字符串(比如 "INSERT INTO user (name) VALUES ('" + v.Interface() + "')")是危险的——它绕过参数绑定,必然导致 SQL 注入。database/sql 的 Query 和 Exec 要求 SQL 是静态或确定性拼接的字面量,而占位符(? 或 :name)必须由驱动在预编译阶段识别。反射生成的字符串里混进 ?,但没同步传参,就会报 sql: expected 0 arguments, got 1。
- ORM(如 gorm、sqlc)只在「元数据映射」阶段用反射:读
db:"user_name"标签,建立字段 ↔ 列名关系 - SQL 构建本身靠预编译语句 +
[]interface{}参数列表,和反射无关 - 若硬要用反射生成 INSERT/SELECT,目标只能是两个切片:
columns []string和args []interface{},而非一整条 SQL 字符串
正确提取结构体字段名与 db 标签映射
核心是遍历结构体所有字段,跳过未导出字段(首字母小写),解析 db tag,并处理 ,inline 嵌套场景。别依赖 FieldByName —— 它对小写字段返回无效值,必须用 NumField() + 循环。
- 调用
reflect.TypeOf(v).NumField()获取字段总数,再用Field(i)逐个取reflect.StructField - 对每个字段,先检查
field.PkgPath == ""(即是否导出),不导出则跳过 - 执行
tag := field.Tag.Get("db");若为空或"-",跳过该字段 - 若
field.Anonymous && strings.Contains(tag, ",inline"),递归处理其内部字段,并用当前字段名作前缀(如"extra_created_at") - 切忌对
tag直接strings.Split(tag, " ")[0]—— 先strings.TrimSpace(tag)防空格导致判空失败
动态生成 INSERT 的 columns 和 args 切片
目标不是拼 SQL,而是产出两样东西:[]string{"id", "name", "created_at"} 和 []interface{}{1, "foo", time.Now()}。这样你才能安全调用 db.Exec("INSERT INTO t ("+strings.Join(columns, ",")+") VALUES ("+strings.Repeat("?,", len(args)-1)+"?)", args...)。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 遍历字段时,同时收集
dbtag 值(列名)和对应字段的reflect.Value - 对每个字段值
v,用v.Interface()获取基础值;但若字段类型实现了driver.Valuer(如自定义时间类型),应显式调用v.Interface().(driver.Valuer).Value() - 遇到指针字段(
v.Kind() == reflect.Ptr)且v.IsNil(),需决定是跳过、填nil,还是生成"col IS NULL"条件 —— 这属于业务逻辑,反射不替你判断 - 别在循环里反复调用
v.FieldByName("X"),开销大且易错;一次性遍历完,存好字段索引或值切片
扫描查询结果到结构体时的地址陷阱
rows.Scan() 必须传指针,而反射中容易犯的错是把循环变量地址当成字段地址,导致所有字段写入同一内存位置。典型错误:for _, val := range values { ptrs = append(ptrs, &val) } —— &val 永远指向同一个变量。
立即学习“go语言免费学习笔记(深入)”;
- 正确做法:用
reflect.New(t.Elem())创建新实例指针,再用StructForScan()辅助函数将其字段地址转为[]interface{} -
StructForScan内部要对每个字段调用field.Addr().Interface(),而不是field.Interface() - 如果结构体含嵌入字段(
BaseModel),且用了db:",inline",需在StructForScan中递归展开,否则Scan会因参数数量不匹配 panic - 性能上,每次查询都做反射遍历是瓶颈;可考虑用
sync.Map缓存字段映射结果,key 为reflect.Type
最常被忽略的点是:反射只解决“字段到列名”的映射和“值到参数”的提取,它不处理主键策略、NULL 语义、事务边界或连接池管理。这些必须由上层代码显式控制,不能指望反射自动补全。

















