最近封装功能发现不得不用到reflect反射,所以找了一些资料学习了一下。

下面我以一个具体的例子,带大家了解真实场景中的反射用法,并且说明反射的核心思路。

需求

封装访问mysql的Query方法,它执行SQL查询并将结果填充到参数中返回,下面是方法的定义:

Go
1
2
// 查询SQL
func Query(result interface{}, sql string, values ...interface{}) error

调用的是这样的:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
type User struct {
Id int
Name string
}
 
func main() {
        result := []*User{}
if err := Query(&result, "select * from user where name=?", "owen"); err == nil {
for i := 0; i < len(result); i++ {
fmt.Println(*result[i])
}
}
}

代码输出了一行记录:

Go
1
{1 owen}

Query方法第1个参数必须传入slice的地址,这样Query内部才能将append扩容过的slice设置到result中,从而调用方可以访问到数据。

第1个参数还要求slice中的元素是指针类型,这是因为Query内部会逐行的向slice中append每一行数据,指针数组在内存扩容分配的时候性能更好。

下面我们来实现Query方法。

实现

首先我必须声明,在Query中我使用了gorm库执行了SQL参数绑定和执行,但是仅仅使用gorm是无法实现Query方法的,我们还需要大量理解与使用反射解决剩余问题。

鉴别*[]*User

空interface{}内部记录了实际数据的type和value,在reflect库对应了2种类型可以获取到interface内的type和value,分别叫做reflect.TypeOf和reflect.ValueOf方法。

我要做的第一件事情就是先检查一下result的type是不是一个指针,因为我要求传入的是slice的地址:

Go
1
2
3
4
5
6
7
// 查询SQL
func Query(result interface{}, sql string, values ...interface{}) error {
// type1是*[]*User
type1 := reflect.TypeOf(result)
if type1.Kind() != reflect.Ptr {
return errors.New("第一个参数必须是指针")
}

我们全部以上述调用代码示例中的参数为例进行代码说明与讲解。

先用TypeOf返回一个Type对象type1,其自身类型是reflect.Type,内部实际保存类型是*[]*User。

Type.kind()方法返回数据类型,reflect.Ptr是其中一种类型定义,*[]*User就是指针,所以这一步合法(红色部分)。

但是指针具体指向的是啥,还需要下面进一步判定。

鉴别[]*User

接下来需要看一下指针指向的是不是1个slice,因为我们mysql查询回来的多行数据需要append到这个slice里面。

在反射中,对一个指针类型解引用只需要调用一下Elem()方法,所以下面的判断代码是这样的:

Go
1
2
3
4
5
// type2是[]*User
type2 := type1.Elem() // 解指针后的类型
if type2.Kind() != reflect.Slice {
return errors.New("第一个参数必须指向切片")
}

type1是*[]*User,所以对type1解引用相当于*type1,得到type2实际上保存的类型就是[]*User。

所以只需要type2.Kind()看一下是不是slice即可。

鉴别*User

作为一个严谨的实现,其实数据的每一层都需要反射判定,比如我们接下来其实需要确认一下slice里面的元素类型是不是指针类型:

Go
1
2
3
4
5
// type3是*User
type3 := type2.Elem()
if type3.Kind() != reflect.Ptr {
return errors.New("切片元素必须是指针类型")
}

如果更加严谨,我们还应该继续对type3解引用,看一下其类型是不是reflect.Struct,但是现在我们就做到这个程度即可。

调用gorm完成查询

Go
1
2
3
4
5
// 发起SQL查询
rows, _ := db.Raw(sql, values...).Rows()
for rows.Next() {
            // 这里面至关重要
}

db.Raw这一行代码是gorm库的方法,传入SQL以及要绑定的参数,它就会帮我们完成查询并返回rows对象。

假设我们没有封装Query方法,那么直接使用gorm是这样使用的:

Go
1
2
3
4
5
6
7
8
// 发起SQL查询
result := []*User{}
rows, _ := db.Raw(sql, values...).Rows()
for rows.Next() {
u := User{}
db.ScanRows(rows, &u)
result = append(result, &u)
}

通过迭代rows,并每次调用ScanRows可以将每一行数据反射到结构体User的对应字段中。

但是现在我们Query方法封装的时候传入的result已经是一个interface,这就需要用反射来实现上述原本很简单的事情。

创建User对象

因为gorm的ScanRows需要传入User结构体进行填充,所以我们需要先通过反射创建一个User类型的对象:

Go
1
2
3
for rows.Next() {
//  type3.Elem()是User, elem是*User
elem := reflect.New(type3.Elem())

因为之前type3是*User,所以对type3解引用可以得到User类型,通过reflect.New可以new一个User类型的对象,返回一个*User地址放到elem里。

调用ScanRows填充User

Go
1
2
// 传入*User
db.ScanRows(rows, elem.Interface())

elem其实是一个reflect.Value变量,内部值是一个创建好的*User。

可以将其转换回到一个空interface{},因为interface可以装任何value以及其type。

所以调用elem.Interface()方法就将其转成了一个好用的interface{},可以作为ScanRows的传参了,因为ScanRows内部也是基于反射支持结构体填充的,所以它的函数定义也是interface{}:

Go
1
2
// ScanRows scan `*sql.Rows` to give struct
func (s *DB) ScanRows(rows *sql.Rows, result interface{}) error {

软件的每一层各司其职,就是这么回事。

将*User append到result中

每一行数据都应该append追加到result中返回,我们还记得result是一个*[]*User吧。

代码如下:

Go
1
2
// reflect.ValueOf(result).Elem()是[]*User,Elem是*User,newSlice是[]*User
newSlice := reflect.Append(reflect.ValueOf(result).Elem(), elem)

reflect.ValueOf可以取到result这个interface{}的value,也就是*[]*User。

通过Elem()解引用后相当于得到了另外1个value,也就是[]*User,也就是传入的slice自身,我们往里追加数据即可。

在反射情况下,Elem()得到value虽然代表了[]*User的值,但其实它是一个reflect.Value。

因此常规的append方法并不能直接用在reflect.Value身上,另外append也不支持interface{}参数,所以我们即便对reflect.Value调用Interface()方法后也不能传给append。

不过官方早就考虑到了,所以提供了一个reflect.Append方法,它接收reflect.Value类型的传参,当然实际其内部反射的是[]*User那个slice。

elem是刚才new分配到的*User,所以上述代码就是将*User追加到了[]*User中,并且返回了扩容后的新slice,行为和append操作一样。

将新slice覆盖到旧slice

如果我们熟悉append,应该知道append后slice可能扩容而导致地址改变,所以使用append的时候总是应该这样:

Go
1
2
3
4
// 假设这是在Query方法内,那么其实相当于在发生下面的代码:
                // result := *[]*User{}
                // u := new(User)
                // *result = append(*result, u)

在有反射的情况下,其实原理也是一样的。

我们需要做的就是把上面的newSlice保存到*result中:

Go
1
2
3
// 扩容后的slice赋值给*result
// reflect.ValueOf(result).Elem()是[]User
reflect.ValueOf(result).Elem().Set(newSlice)

reflect.ValueOf(result).Elem()相当于解引用得到了[]*User这个slice,也就是*result。

Set方法相当于*result = newSlice。

相关资料

如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~