反射更新SQL需先Elem()解引用指针,遍历字段时检查IsValid()和!IsZero(),用db tag映射列名并参数化占位符,主键须显式指定,NULL语义需用指针或sql.NullString区分。

反射获取指针对象的非零字段值
用 reflect.ValueOf 处理指针时,必须先 .Elem() 解引用,否则拿不到结构体字段;同时要跳过零值(比如 0、空字符串、nil),否则会把默认值也写进 SQL。常见错误是直接遍历 reflect.TypeOf 的字段名,却没检查对应 reflect.Value 是否为零。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
v := reflect.ValueOf(ptr).Elem()获取实际值,再用t := reflect.TypeOf(ptr).Elem()获取类型 - 遍历字段前,先判断
v.Field(i).IsValid() && !v.Field(i).IsZero() - 注意嵌套结构体或指针字段:如果字段本身是指针(如
*string),IsZero()判的是指针是否为nil,不是它指向的值 - 导出字段才可被反射访问——未导出字段(小写开头)会被跳过,不会出现在补丁里
生成 SET 子句时处理字段名映射与 SQL 转义
Go 结构体字段名通常用 CamelCase,但数据库列名多为 snake_case,不映射会拼出错列名;更关键的是,字段值必须参数化,绝不能拼进 SQL 字符串。常见错误是用 fmt.Sprintf("SET %s = '%v'", col, val),这既不安全也不支持 NULL 或二进制数据。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用 struct tag(如
db:"user_name")提取列名,没 tag 时用snakecase工具库自动转换字段名 - 只收集非零字段的列名和占位符(如
user_name = ?),值统一存入[]interface{}切片 - 对
time.Time、sql.NullString等特殊类型,需额外判断:比如sql.NullString要用.Valid判断是否写入,而不是.IsZero() - 避免在反射循环里反复调用
strings.ToLower或正则——提前缓存映射表更高效
构建带 WHERE 条件的安全 UPDATE 语句
补丁更新必须带主键或唯一约束条件,否则可能误更新整张表。反射本身不提供主键信息,得靠约定(比如 tag 标记 db:",primary")或外部传入 ID。常见错误是硬编码 WHERE id = ?,但有些表用 uuid 或复合主键。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 主键字段应单独传入(如
UpdatePatch(obj, "id", 123)),或从结构体 tag 中识别(db:",primary"或gorm:"primaryKey") - WHERE 条件值也要进参数切片,和 SET 值合并为统一参数列表,交给
db.Exec(sql, args...)执行 - 别用反射去“猜”主键:没有明确标记时直接报错,比静默生成危险 SQL 强得多
- 若支持软删除(如
deleted_at IS NULL),这个条件必须显式加在 WHERE 后,不能依赖反射推断
处理 NULL 和零值语义冲突
Go 的零值(""、0、false)和数据库的 NULL 是两回事。用户想清空一个字段(设为 NULL),但反射看到的是零值,就会跳过——结果字段原值保留,不是预期的“清空”。这是最隐蔽也最容易线上出问题的点。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 明确区分“未设置”和“设为空”:用指针类型(
*string)或sql.NullString,它们的nil表示NULL,而零值仅表示默认值 - 如果业务允许,强制要求补丁结构体字段全用指针类型,这样
nil就可安全转成NULL,非nil再检查是否为零值 - 不要给字段加
omitemptyJSON tag 并误以为它对 SQL 有效——反射不读 JSON tag - 测试时一定要覆盖“传
nil *string”和“传&""”两种情况,确认生成的 SQL 分别是col = NULL还是col = ''
真正难的不是遍历字段,而是准确表达“用户到底想改什么”:是忽略某个字段?设为空字符串?还是设为 NULL?这层语义必须由类型系统承载,反射只是执行工具。

















