Redigo issue 487
How to scan struct with nested fields?#487
为了更好的了解本篇文章,倡议先浏览issue原文
一、问题是什么
将HGETALL
命令返回的数据,解析到对应的构造体UserInfo
中,然而构造体中的*LiteUser
字段的数据未能胜利解析。
如果将 *LiteUser
改为 LiteUser
就能够了。
二、复现问题
- copy issue 中的代码,go module 装置 redigo,再筹备一台redis服务。
- go.mod 中的 redigo中的版本设置为issue未修改前的版本:v1.8.1。
- 运行代码,会复现此issue的问题,
*LiteUser
字段的数据未能胜利解析。
试试最新版的代码,运行下来的状况:
- go.mod 中的 redigo 中的版本设置为issue最新版本:v1.8.8。
- 运行代码,问题没有呈现。
留神,为了在最新版本下复现问题,需在示例代码大概 73行上面,退出如下代码(前面再回过头来看看这个问题):
...var newUser UserInfonewUser.LiteUser = &LiteUser{}...
三、怎么解决的
具体内容详见 pr 490
在看如何解决之前,先梳理一下执行流程:
3.1 解析数据到构造体变量
当执行HGETALL
从 Redis 中拿到了数据后,须要将数据解析到构造体的成员变量上,就像从 MySQL 拿进去数据,解析到构造体成员变量上是一个意思。
Redigo 提供好了一个办法,将数据和构造体变量传进去,数据就会解析到newUser
构造体上:
redis.ScanStruct(v, &newUser)
3.2 ScanStruct
接下来,看下redis.ScanStruct()
都做了些什么。
我梳理总结了一下过程中调用的办法:
// 将数据解析到structSpecForType返回的构造体成员上func ScanStruct(src []interface{}, dest interface{}) error { //获取变量指针 d := reflect.ValueOf(dest) //获取指针指向的变量 d = d.Elem() structSpecForType(d.Type()) ...}// 依据传入的reflect.Type,先去缓存中查找是否解析过,如果没有调用compileStructSpecfunc structSpecForType(t reflect.Type) *structSpec { ... compileStructSpec(t, make(map[string]int), nil, ss) ...}
3.3 compileStructSpec
compileStructSpec
办法实现的就是类型解析,问题其实就出在了这。
先将梳理过的总结贴出来:
- 应用反射将数据解析到
&newUser 构造体
的所有成员变量 - 在V1.8.1版本及以前,只解析了 reflect.Struct(LiteUser),未解决 reflect.Ptr(*LiteUser)
- 在V1.8.2 版本及当前,减少了 reflect.Ptr 的判断
上面是外围逻辑
修复前:
func compileStructSpec(t reflect.Type, depth map[string]int, index []int, ss *structSpec) { // t.NumField()获取构造体类型的所有字段的个数 for i := 0; i < t.NumField(); i++ { // t.Field()返回指定的字段,类型为 StructField f := t.Field(i) switch { // f.PkgPath 包门路不为空 且 不是匿名函数 // f.Anonymous 示意该字段是否为匿名字段 case f.PkgPath != "" && !f.Anonymous: // 疏忽未导出的:构造体中的某个成员改为小写(公有),就会进到这个case // Ignore unexported fields. // UserInfo中的成员LiteUser,并未设置 name,为匿名字段,就会进到这个case case f.Anonymous: // f.Type.Kind() 获取品种 // 如果以后type为构造体,进行递归调用,以解决以后type内所有构造体成员 // 对于 `LiteUser` 会进到这个 case if f.Type.Kind() == reflect.Struct { compileStructSpec(f.Type, depth, append(index, i), ss) }
修复后:
...func compileStructSpec(t reflect.Type, depth map[string]int, index []int, ss *structSpec) {LOOP: for i := 0; i < t.NumField(); i++ { f := t.Field(i) switch { case f.PkgPath != "" && !f.Anonymous: // Ignore unexported fields. case f.Anonymous: switch f.Type.Kind() { case reflect.Struct: compileStructSpec(f.Type, depth, append(index, i), ss) // 这里是变动的局部,对于 `*LiteUser` 会进到这个 case case reflect.Ptr: // 如果以后字段的type的值为构造体,进行递归调用,以解决以后字段内所有构造体成员 // f.Type.Kind()返回的是前f的品种,也就是reflect.Ptr // f.Type.Elem().Kind() 返回的是前f的值的品种,也就是reflect.Struct // TODO(steve): Protect against infinite recursion. if f.Type.Elem().Kind() == reflect.Struct { compileStructSpec(f.Type.Elem(), depth, append(index, i), ss) } }...
OK~,问题解决!
四、扩大
4.1 反射
compileStructSpec
办法外部,次要就是通过反射来实现的。
这里重点要说下,为啥d := reflect.ValueOf(dest)
完了之后,还要用d = d.Elem()
,援用《Go 语言设计与实现》的一句话
因为 Go 语言的函数调用都是值传递的,所以咱们只能只能用曲折的形式扭转原变量:先获取指针对应的reflect.Value
,再通过reflect.Value.Elem
办法失去能够被设置的变量。
参考
Go 语言波及与实现-反射
4.2 newUser.LiteUser = &LiteUser{}
int、string等为值类型的,即便不进行初始化,只有申明,值也会默认成这个类型的“零”值。
然而像 Map、Slice、Channel等援用变量,须要在应用前先make()
的。
同理&
类型的变量,他的值是存储的是内存地址,那就必须先要初始化一个LiteUser
的构造体,而后将他的内存地址,赋值给newUser.LiteUser
,能力失常应用。