必须传结构体指针并调用Elem():reflect.TypeOf(entity).Name()取表名,reflect.ValueOf(entity).Elem()后遍历字段,用field.Tag.Get("db")取列名,仅导出字段且CanSet()为true才可写入,Kind()比Name()更可靠用于类型映射。

如何用 reflect.TypeOf 和 reflect.ValueOf 获取结构体元信息
ORM 框架必须在运行时知道一个 User 结构体有哪些字段、类型、标签值,而不是靠硬编码。关键不是“能不能”,而是“怎么取才安全高效”。
必须传入结构体指针(如 &user),否则 reflect.ValueOf(user) 返回的是不可寻址的副本,后续无法写入字段值;而 reflect.TypeOf(user) 虽然能取到类型,但若传入 nil 指针会 panic。
-
reflect.TypeOf(entity).Name()获取结构体名(用于表名推导),注意它不带包路径 -
reflect.ValueOf(entity).Elem()是处理指针的必要步骤,否则NumField()会返回 0 - 遍历字段时用
field.Tag.Get("db")提取映射列名,空字符串表示该字段不参与映射 - 别直接用
field.Name作列名——Go 字段首字母大写是导出要求,和数据库小写列名习惯冲突
为什么 FieldByNameFunc + tag 匹配比字符串精确匹配更可靠
查询结果列名来自数据库(如 created_at),而 Go 字段名是 CreatedAt,靠大小写转换或下划线转驼峰极易出错。用 FieldByNameFunc 配合 db 标签才是正解。
常见错误是忽略字段是否可设置:field.CanSet() == false 通常因为传入了非指针、字段未导出(小写开头)、或 Elem() 没调用到位。
- 匹配逻辑必须同时满足:字段存在 +
db标签值等于当前列名 +CanSet()为 true - 不要用
strings.EqualFold做列名比较——PostgreSQL 默认小写,MySQL 可能大小写敏感,依赖数据库行为不可控 - 如果列名在结构体中无对应
db标签字段,应明确报错(如"no matching field for column created_at"),而不是静默跳过
DataTypeOf 实现里最容易忽略的类型映射边界
Go 的 int、int32、int64 在 SQLite 全映射为 INTEGER,但在 PostgreSQL 中 int64 应对应 BIGINT,否则插入超限值会失败。Dialect 层不能只看 Go 类型名,得结合 Kind() 和具体数值范围判断。
反射拿到字段值后,value.Kind() 比 value.Type().Name() 更底层、更稳定——比如自定义类型 type UserID int64,Name() 返回 UserID,但 Kind() 仍是 Int64。
- 优先用
value.Kind()分支判断:对reflect.Int64映射BIGINT,reflect.Int映射INTEGER -
time.Time必须特殊处理:SQLite 用TEXT存 RFC3339,PostgreSQL 用TIMESTAMP WITH TIME ZONE - 切片(
[]byte)和字符串都映射为TEXT或BLOB,但需根据value.Type().Elem().Kind()判断是否是字节切片
Scan 填充时 value.Addr().Interface() 的坑在哪
执行 rows.Scan() 要求传入的是“地址列表”,即 []interface{},每个元素必须是字段地址(*string、*int64 等)。用反射生成这个列表时,field.Addr().Interface() 是唯一正确方式。
错误做法包括:field.Interface()(传值而非地址)、reflect.ValueOf(&field).Interface()(嵌套指针)、或手动构造 *int64 类型变量(失去泛型能力)。
- 必须确保
field来自reflect.Indirect(reflect.ValueOf(dest)).Elem(),否则Addr()会 panic - 对非导出字段(小写开头),
field.CanAddr()返回 false,此时应跳过或报错,不能强行取地址 - 扫描前要检查字段类型是否兼容:比如数据库返回
NULL时,目标字段需是sql.NullString或可接受 nil 的接口,否则 panic
真正难的不是“怎么让反射跑起来”,而是当结构体嵌套、字段含 interface{}、或数据库列动态增减时,反射路径是否仍可控。这些场景下,缓存 reflect.Type 和字段索引比每次重新遍历快一个数量级,但缓存键设计稍有不慎就会导致内存泄漏——这是多数人调试三天才发现的点。











