浅尝Golang反射
本文最后更新于 2024年6月7日 下午
0. 前言
对反射的尝试来源于项目中某个CRDU
功能的开发,当时我使用Go
做一个WEB
项目的后端开发,数据库选用的是MongoDB
,功能其实也非常的简单,查询数据库某个表中的一条数据,并将得到的数据返回。使用伪代码描述逻辑即为:
1 |
|
这个代码确实很简单,当时也没多考虑。当随着项目的推进,出现了大量逻辑相似的代码,他们的唯一不同之处在于:每个函数需要返回的数据类型不同。随着相似代码的多次出现,我实在忍无可忍,开始思考是否有一种办法可以将其中相似逻辑的代码进行复用,提高代码的利用率。
我第一个想到的办法就是泛型,但是Go
并没有为开发者提供泛型的功能,所以这条路堵死了。于是我想到了空接口,任何类型都实现了空接口,虽然每个函相似数返回的都是不同数据的数组,但只要将返回值的类型改为空接口,就可以解决返回值的问题了。
但是这样并没有解决全部的问题,因为在所有代码中有一步是必须的:
1 |
|
如何根据传参声明不同类型的数据(data_A
)和不同类型的数组(list_A
),是我必须解决的。这个问题一时毫无头绪,不知该如何解决。所以我决定问一问BBFat
同学的意见,他立刻想到:我的这个需求需要在运行时,动态的修改程序(修改程序中声明的变量类型),无疑这就是反射这个特性应该解决的事情,所以我的这个需求无疑是使用反射进行解决。最终经过BBfat
的帮助和反复实验,给出了如下的代码:
1 |
|
1. 反射 reflect
包
- 官方文档:Package reflect
- 社区文档:中文翻译
1.1. reflect.Type
Type
是一个接口类型,用来表示一个go类型,该接口拥有以下一系列方法:
1 |
|
1.1.1. kind
方法解析
其他方法都比较好理解,其中容易存在歧义的是Kind
方法,该方法返回的是变量的基础类型,何为基础类型请参见下面的例子:
1 |
|
Go
中可以定义千万种数据类型,但是Go
中定义了26中基础类型,无论是何种利用struct
定义的类型,他们的基础类型都是struct
,Kind
方法的返回值也即是这26中基础类型之一:
1 |
|
1.1.2. 常用方法
关于reflect.Type
的方法,一般都是只读方法,所以很少使用(我认为的…)。
相反,另外一些方法则经常用到:
1 |
|
其中reflect.TypeOf
接受一个空接口类型,此时根据传入的实参变量,Typeof
方法的返回值可分为两种情况:
- 如果是一个绑定了实例的接口变量或者是一个实例变量,则返回实例的
Type
- 如果是一个未绑定实例的空接口,则返回接口
interface
的Type
1.2. reflect.Value
reflect.Value
是Go
反射的核心,他保存着实例值的信息,因为相比于reflect.Type
他提供了更多“写”方法,使用其才可以做到在运行时动态地修改程序。reflect.Value
是一个struct
并且向开发者提供了丰富的方法:
1 |
|
1.2.1. 注意事项
MakeSlice
与SliceOf
搭配使用MakeMap
与MapOf
搭配使用MakeChan
与ChanOf
搭配使用Elem
与Indirect
都可以将指针类型的Value
值转化为指针指向类型的Value
值,但Elem
会导致panic
Interface
方法可以将Value
值对应的实例绑定到空接口。- 使用
Set
方法,可以实例对应的Value
值改变,进而改变实例值。
2. 反射规则
2.1. 转化规则
-
实例可以与
Value
相互转换- 实例到
Value
1
func ValueOf(i interface{}) Value
Value
到实例
1
func (v Value) Interface() (i interface{})
或
1
2func (v Value) CanSet() bool
func (v Value) Set(x Value)参照下面的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main
import (
"fmt"
"reflect"
)
type person struct {
age int
Name string
}
func main() {
var someone person = person{age: 22, Name: "han"}
val := reflect.ValueOf(&someone) // 将指向实例的指针绑定到接口上传入ValueOf方法,因为Go中的函数传参都是值拷贝,因此这时接口绑定的是指针值的拷贝
fmt.Println(val.CanSet()) // false 作为副本的指针值的拷贝,是不能进行Set的,因为是在副本上进行操作
val = val.Elem() // 将指针的Value转换为指针指向的实例的Value
fmt.Println(val.CanSet()) // true 这时是在原实例上进行操作,因此可以进行Set
if val.CanSet() {
fmt.Println(val.FieldByName("age").CanSet()) // false struct中以小写名字开头的字段,外部是不可见的,因此无法Set
fmt.Println(val.FieldByName("Name").CanSet()) // true
newName := reflect.ValueOf("meng")
val.FieldByName("Name").Set(newName)
}
fmt.Println(someone) // {22 meng}
} - 实例到
-
实例可以与
Type
相互转换- 实例到
Type
1
func TypeOf(i interface{}) Type
Type
到实例
1
2func New(typ Type) Value
func (v Value) Interface() (i interface{}) - 实例到
-
Value
可以与Type
相互转换Value
到Type
1
func (v Value) Type() Type
Type
到Value
1
func New(typ Type) Value
-
指针类型的
Value
可以与值类型的Value
相互转换- 指针类型的
Value
到值类型的Value
1
func (v Value) Elem() Value
或
1
func Indirect(v Value) Value
- 值类型的
Value
到指针类型的Value
1
func (v Value) Addr() Value
- 指针类型的
-
指针类型的
Type
可以与值类型的Type
相互转换- 指针类型的
Type
到值类型的Type
1
func (t Type) Elem() Type
- 值类型的
Type
到指针类型的Type
1
func (t Type) PtrTo() Type
- 指针类型的
2.2. 反射三定律
- 反射可以从接口值得到反射对象
- 反射可以从反射对象得到接口值
- 若要修改一个反射对象,则其值必须可以修改