传结构体指针是硬性要求,因reflect.value需可寻址且可设置才能赋值;必须调用.elem()获取实例,检查.isvalid()和.canset(),并正确解析db标签、保持字段遍历顺序、区分kind处理类型。

Go 语言里学反射不是为了炫技,而是为了解决 ORM 这类“结构体 ↔ SQL ↔ 行数据”三端动态对齐的问题;但直接啃 reflect 包文档容易走偏——真正卡住人的从来不是 API 怎么调,而是字段能不能写、标签怎么取、顺序怎么保、零值怎么判。
为什么传结构体指针是硬性要求,不是可选习惯
ORM 的 scan 或 insert 都要改结构体字段值,而 reflect.Value 只有在可寻址(addressable)且可设置(can set)时才能写入。传 User{} 进去,reflect.ValueOf(u).FieldByName("ID") 返回的 Value 永远不可设;传 &u 才行。但光传指针还不够:
-
reflect.ValueOf(&u)得到的是指针类型的Value,必须调用.Elem()才能拿到结构体实例本身 - 如果传的是
nil指针,.Elem()会 panic,得先用.IsValid() && .CanInterface()做防护 - 字段是否可设还要再过一道
field.CanSet(),小写开头的字段(未导出)永远过不了这一关
db 标签解析不能只靠 field.Tag.Get("db")
看似一行代码就能取标签,但实际要处理三种边界情况:
-
db:""和db:"-"都表示忽略该字段,不能当成有效列名用 - 字段名是
CreatedAt,但数据库列是created_at,靠strings.ToLower(field.Name)硬转会错配——必须依赖field.Tag.Get("db")的显式声明 - 多个字段写同一个
db标签(比如两个字段都标db:"id"),FieldByNameFunc会返回第一个匹配项,但你得意识到这是歧义设计,应提前校验
SQL 参数顺序和字段遍历顺序必须严格一致
Go 规范保证 reflect.Type.NumField() 返回的字段顺序就是源码定义顺序,这让你能安全地按序生成 INSERT INTO t (a,b,c) VALUES (?,?,?)。但陷阱在于:
- 字段过滤逻辑(跳过非导出字段、跳过
db:"-")必须和 SQL 构建、参数传入两处完全同步,否则stmt.Exec()会报sql: expected X arguments, got Y - 绝不能用
map[string]int缓存字段索引——map 遍历无序,会导致列名和参数错位 - 嵌套结构体(如
User.Profile *Profile)若参与映射,需递归展开并维护全局字段顺序,否则外键或 JSON 字段容易脱节
time.Time 和自定义类型映射最容易漏掉 Kind() 判断
字段类型是 type UserID int64,field.Type().Name() 返回 UserID,但数据库类型该映射成 BIGINT 而不是 UserID。同理,time.Time 在 PostgreSQL 和 SQLite 中的存储格式完全不同:
- 优先用
field.Kind()分支判断:所有整数类统一走reflect.Int*,再结合具体数值范围决定 DB 类型 -
time.Time必须单独处理——SQLite 用 TEXT 存 RFC3339,PostgreSQL 用TIMESTAMP WITH TIME ZONE,MySQL 用DATETIME - 遇到
sql.NullString这类包装类型,要递进一层看.Elem().Kind(),否则会把整个 struct 当作普通类型映射
真正难的不是写出第一版反射 ORM,而是让 Scan 不 panic、Insert 不丢字段、Update 不误写零值、跨 dialect 不翻车——这些细节全藏在 CanSet()、Elem()、Kind() 和标签校验的组合里,少一个条件就埋一个 runtime bug。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











