reflect.StructTag 是唯一可靠字段元信息来源——Go 的 reflect 包不保存字段名映射、忽略规则、默认值等业务语义,必须通过结构体 tag(如 db:"user_id")显式声明;json tag 不足,需统一约定 db tag;非导出字段、匿名嵌入、time.Time、指针等须特殊处理;UPDATE 的 WHERE 和 SQL 占位符需显式指定方言与条件。

反射生成 INSERT 语句时,reflect.StructTag 是唯一可靠字段元信息来源
Go 的 reflect 包本身不保存字段名映射、是否忽略、默认值等业务语义,必须靠结构体 tag 显式声明。不依赖第三方库时,json tag 不够用(比如字段名不一致、有嵌套、需支持 NULL),得统一约定用 db tag。
常见错误是直接用 field.Name 当列名,结果生成 INSERT INTO user (UserID, UserName) ... —— 大驼峰列名在 PostgreSQL/MySQL 里要么报错,要么查不到数据。
- 强制要求所有可入库字段带
db:"user_id"或db:"user_id,omitempty" -
omitempty表示零值跳过(注意:指针 nil、字符串 ""、整型 0 都算零值,需按业务判断是否真要跳) - 遇到匿名嵌入结构体(如
type User struct { Base }),必须手动递归处理,reflect默认不展开 - 别信
field.Tag.Get("json")能通用——API 返回用user_name,DB 列可能是user_name_encrypted
reflect.Value.Interface() 直接转 SQL 值前必须做类型分流
把任意 reflect.Value 往 SQL 字符串里拼,不出错才怪。比如 time.Time 输出的是 Go 内部格式,MySQL 会报 Incorrect datetime value;*string 如果是 nil,直接 .Interface() 得到 nil 指针,再 fmt.Sprintf("%v") 就 panic。
必须按底层类型分支处理:
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
-
reflect.String→ 用strconv.Quote()包裹单引号,防 SQL 注入(哪怕你确定输入干净,协议层也要求引号) -
reflect.Int*, reflect.Uint*, reflect.Float*→ 直接fmt.Sprintf("%d", v.Interface()),不加引号 -
reflect.Ptr→ 先v.IsNil()判断,nil 输出NULL,否则递归取v.Elem() -
reflect.Struct且是time.Time→ 调v.Interface().(time.Time).Format("2006-01-02 15:04:05") - 其余未覆盖类型(如
sql.NullString)直接 panic 并提示“不支持的字段类型”,别静默转成"%!s(MISSING)"
生成 UPDATE 语句时,WHERE 条件不能靠反射自动推断
纯反射无法知道哪几个字段构成主键或唯一索引。有人试过扫描所有字段找 db:",primary",但这是危险设计:一个结构体可能对应多张表,主键字段不同;或者软删除字段(deleted_at)不该进 WHERE。
实操中必须显式传参:
- UPDATE 函数签名得是
BuildUpdate(v interface{}, where map[string]interface{}) (sql string, args []interface{}) -
wheremap 的 key 必须严格匹配dbtag 值(如{"user_id": 123}),不接受结构体字段名 - 不允许把整个 struct 当 WHERE —— 否则
User{Name:"admin", ID:0}会生成WHERE id = 0,删光表 - 如果真想省事,就额外定义
func (u *User) PrimaryKey() map[string]interface{} { return map[string]interface{}{"user_id": u.ID} },反射只调这个方法
零依赖 ≠ 零配置,database/sql 的 Placeholder 差异必须硬编码适配
MySQL 用 ?,PostgreSQL 用 $1,SQLite 看编译选项。反射生成 SQL 时若不处理占位符,写死 ? 就只能跑 MySQL。
最简方案是加一个参数指定方言:
- 传
"mysql"→ 生成INSERT INTO t (a,b) VALUES (?, ?) - 传
"postgres"→ 生成INSERT INTO t (a,b) VALUES ($1, $2),且参数顺序必须和字段遍历顺序严格一致 - 别尝试自动生成
$1,$2,...,$n后再排序——反射遍历字段顺序是确定的(按源码顺序),但如果你用了map存字段名→值,顺序就丢了 - 更狠的坑:
postgres的VALUES ($1, $2)要求args是扁平 slice,而mysql的?可以复用同一份[]interface{},但postgres下args必须和占位符数量、顺序完全对齐
字段顺序错一位,PostgreSQL 就报 ERROR: bind message supplies 3 parameters, but prepared statement "" requires 4,这种错只能 runtime 发现。

















