关于redis:Redigo-ScanStruct匿名指针字段的解析

50次阅读

共计 2895 个字符,预计需要花费 8 分钟才能阅读完成。

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,能力失常应用。

正文完
 0