Redigo:|Redigo: ScanStruct()匿名指针字段的解析
文章图片
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
,才能正常使用。文章图片
推荐阅读
- func-spring-boot-starter|func-spring-boot-starter 匿名函数托管
- Redigo:|Redigo: Envoy代理Redis场景下的错误提示
- Linux操作系统|【Linux】进程间通信(匿名管道、命名管道、共享内存)
- Docker|Docker容器数据卷、数据卷容器及具名和匿名挂载
- Docker|Docker第五篇【容器数据卷、匿名和具名挂载、DockerFile】
- docker|docker 容器数据卷+具名挂载匿名挂载
- JavaScript|JavaScript 匿名函数、模块模式、闭包、命名空间、创建构造器(类)、继承
- Javascript自执行匿名函数(function() { })()的原理浅析
- javascript中为何在匿名function函数后面还外加一个括号
- Web前端|轻松理解JavaScript匿名函数、自执行函数、闭包函数、回调函数