structs.Names()不能直接用于生成SQL列名,因其返回Go字段名而非数据库列名,且忽略db标签和非导出字段;正确做法是用反射遍历StructField并提取db标签。

structs.Names() 不能直接用于生成 SQL 列名
很多人看到 structs.Names() 返回字段名切片,就直接拼成 "SELECT " + strings.Join(structs.Names(User{}), ", ") + " FROM users",这会出错。因为 structs.Names() 返回的是 Go 字段名(如 UserName),不是数据库列名(如 user_name);且它不读取 db 标签,也不跳过非导出字段或 - 标签字段。
真正该做的是遍历 reflect.StructField,手动提取 Tag.Get("db"):
- 用
reflect.TypeOf(u).NumField()遍历所有字段 - 对每个
field,先检查field.PkgPath == ""(确保是导出字段) - 再取
tag := field.Tag.Get("db"),若为空或为"-"就跳过 - 若
field.Anonymous && tag == ",inline",需递归处理其字段,并加前缀(如"extra_")
INSERT 语句中 VALUES(?, ?, ?) 的参数数量必须与字段严格对齐
反射生成占位符时,常见错误是只数了字段个数,却没同步过滤掉被忽略的字段(比如 CreatedAt time.Time `db:"-"`),导致 rows.Scan() 或 stmt.Exec() 报 sql: expected 3 arguments, got 2。
正确做法是:字段遍历和占位符生成必须走同一套逻辑分支:
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 声明两个切片:
columns := []string{}和placeholders := []string{} - 每次进入有效字段分支(导出 + 有非空 db tag),才同时
append(columns, dbTag)和append(placeholders, "?") - 最后用
strings.Join(columns, ", ")和strings.Join(placeholders, ", ")拼 SQL
Scan 结果到结构体字段时,必须传入可寻址的字段地址
这是最常踩的坑:想用反射把查询结果写进结构体,却写了 &val(循环变量地址),导致所有字段指向同一内存,数据全被覆盖。
关键点在于:sql.Rows.Scan() 要的是「字段地址」,不是「字段名地址」,也不是「临时变量地址」:
- 错误写法:
for i, val := range fields { ptrs[i] = &val }——val是复用变量,&val永远相同 - 正确写法(推荐):
v := reflect.ValueOf(&user).Elem(),然后v.Field(i).Addr().Interface()得到每个字段的真实地址 - 注意:
v.Field(i)必须可寻址,所以user必须是变量(不能是字面量或返回值),且需用&user构造指针再.Elem()
反射拼 SQL 字符串本身不安全,必须配合参数化查询
别用 fmt.Sprintf("INSERT INTO %s VALUES ('%s')", table, user.Name) 这类方式——哪怕用了反射提取字段,只要把值塞进字符串里,就是 SQL 注入温床。
真正安全的做法是两段分离:
- SQL 字符串只含列名和固定占位符(如
"INSERT INTO users (name, age) VALUES (?, ?)"),由反射生成,但**不含任何用户数据** - 用户数据全部走
[]interface{}参数列表,由db.Exec(sql, args...)自动绑定 - 字段值提取也应通过
reflect.Value.Field(i).Interface(),而不是fmt.Sprintf强转
嵌套结构、自定义类型(如实现了 driver.Valuer 的 UserID)会在这里暴露问题:直接 .Interface() 可能绕过其 Value() 方法。稳妥做法是显式判断并调用:if v.CanInterface() { val := v.Interface(); if vv, ok := val.(driver.Valuer); ok { val, _ = vv.Value() } }。

















