Redigo issue 487

How to scan struct with nested fields?#487
为了更好的了解本篇文章,倡议先浏览issue原文

一、问题是什么

HGETALL 命令返回的数据,解析到对应的构造体UserInfo中,然而构造体中的*LiteUser字段的数据未能胜利解析。

如果将 *LiteUser 改为 LiteUser 就能够了。

二、复现问题

  1. copy issue 中的代码,go module 装置 redigo,再筹备一台redis服务。
  2. go.mod 中的 redigo中的版本设置为issue未修改前的版本:v1.8.1。
  3. 运行代码,会复现此issue的问题,*LiteUser字段的数据未能胜利解析。

试试最新版的代码,运行下来的状况:

  1. go.mod 中的 redigo 中的版本设置为issue最新版本:v1.8.8。
  2. 运行代码,问题没有呈现。
留神,为了在最新版本下复现问题,需在示例代码大概 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,能力失常应用。