Redigo issue 487
How to scan struct with nested fields?#487
为了更好的理解本篇文章,建议先阅读issue原文
一、问题是什么
将HGETALL
命令返回的数据,解析到对应的结构体UserInfo
中,但是结构体中的*LiteUser
字段的数据未能成功解析。
如果将 *LiteUser
改为 LiteUser
就可以了。
二、复现问题
- copy issue 中的代码,go module 安装 redigo,再准备一台redis服务。
- go.mod 中的 redigo中的版本设置为issue未修改前的版本:v1.8.1。
- 运行代码,会复现此issue的问题,
*LiteUser
字段的数据未能成功解析。
试试最新版的代码,运行下来的情况:
- go.mod 中的 redigo 中的版本设置为issue最新版本:v1.8.8。
- 运行代码,问题没有出现。
注意,为了在最新版本下复现问题,需在示例代码大约 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
方法得到可以被设置的变量。
4.2 newUser.LiteUser = &LiteUser{}
int、string等为值类型的,即使不进行初始化,只有声明,值也会默认成这个类型的“零”值。
但是像 Map、Slice、Channel等引用变量,需要在使用前先make()
的。
同理&
类型的变量,他的值是存储的是内存地址,那就必须先要初始化一个LiteUser
的结构体,然后将他的内存地址,赋值给newUser.LiteUser
,才能正常使用。