共计 2895 个字符,预计需要花费 8 分钟才能阅读完成。
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
办法失去能够被设置的变量。
参考
Go 语言波及与实现 - 反射
4.2 newUser.LiteUser = &LiteUser{}
int、string 等为值类型的,即便不进行初始化,只有申明,值也会默认成这个类型的“零”值。
然而像 Map、Slice、Channel 等援用变量,须要在应用前先 make()
的。
同理 &
类型的变量,他的值是存储的是内存地址,那就必须先要初始化一个 LiteUser
的构造体,而后将他的内存地址,赋值给newUser.LiteUser
,能力失常应用。