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

运维救火
• 阅读 821

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,才能正常使用。

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

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
ELK学习笔记之配置logstash消费kafka多个topic并分别生成索引
0x00 filebeat配置多个topicfilebeat.prospectors:input_type:logencoding:GB2312fields_under_root:truefields:添加字段
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Easter79 Easter79
3年前
SpringBoot整合Redis乱码原因及解决方案
问题描述:springboot使用springdataredis存储数据时乱码rediskey/value出现\\xAC\\xED\\x00\\x05t\\x00\\x05问题分析:查看RedisTemplate类!(https://oscimg.oschina.net/oscnet/0a85565fa
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这