Redigo: ScanStruct()匿名指针字段的解析

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 UserInfo
newUser.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,先去缓存中查找是否解析过,如果没有调用compileStructSpec
func 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,才能正常使用。

image.png

你可能感兴趣的:(Redigo: ScanStruct()匿名指针字段的解析)