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

Redigo:|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字段的数据未能成功解析。
【Redigo:|Redigo: ScanStruct()匿名指针字段的解析】试试最新版的代码,运行下来的情况:
  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,才能正常使用。
Redigo:|Redigo: ScanStruct()匿名指针字段的解析
文章图片

    推荐阅读