最近封装功能发现不得不用到reflect反射,所以找了一些资料学习了一下。
下面我以一个具体的例子,带大家了解真实场景中的反射用法,并且说明反射的核心思路。
需求封装访问mysql的Query方法,它执行SQL查询并将结果填充到参数中返回,下面是方法的定义:
1 2 |
// 查询SQL func Query(result interface{}, sql string, values ...interface{}) error |
调用的是这样的:
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]) } } } |
代码输出了一行记录:
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的地址:
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()方法,所以下面的判断代码是这样的:
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里面的元素类型是不是指针类型:
1 2 3 4 5 |
// type3是*User type3 := type2.Elem() if type3.Kind() != reflect.Ptr { return errors.New("切片元素必须是指针类型") } |
如果更加严谨,我们还应该继续对type3解引用,看一下其类型是不是reflect.Struct,但是现在我们就做到这个程度即可。
调用gorm完成查询
1 2 3 4 5 |
// 发起SQL查询 rows, _ := db.Raw(sql, values...).Rows() for rows.Next() { // 这里面至关重要 } |
db.Raw这一行代码是gorm库的方法,传入SQL以及要绑定的参数,它就会帮我们完成查询并返回rows对象。
假设我们没有封装Query方法,那么直接使用gorm是这样使用的:
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类型的对象:
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
1 2 |
// 传入*User db.ScanRows(rows, elem.Interface()) |
elem其实是一个reflect.Value变量,内部值是一个创建好的*User。
可以将其转换回到一个空interface{},因为interface可以装任何value以及其type。
所以调用elem.Interface()方法就将其转成了一个好用的interface{},可以作为ScanRows的传参了,因为ScanRows内部也是基于反射支持结构体填充的,所以它的函数定义也是interface{}:
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吧。
代码如下:
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的时候总是应该这样:
1 2 3 4 |
// 假设这是在Query方法内,那么其实相当于在发生下面的代码: // result := *[]*User{} // u := new(User) // *result = append(*result, u) |
在有反射的情况下,其实原理也是一样的。
我们需要做的就是把上面的newSlice保存到*result中:
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。
相关资料如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~