反射是指在程序运行期对程序本身进行访问和修改的能力,即可以在运行时动态获取变量的各种信息。Go的反射是通过接口的类型信息实现的,即反射建立在接口类型的基础上:当向接口变量赋予一个实体类型的时候,接口会存储实体的类型信息(Type)和值信息(Value)。因此我们可以在运行时利用反射
1.1 类型系统
绝大多数编程语言的类型系统都是类似的,会有声明类型、实际类型之类的分别。
在 Go 反射里面,一个实例可以看成两部分:
- 值
- 实际类型
1.2 反射reflect的核心的两个API
- reflect.Value:用于操作值,部分值是可以被反射修改的
- reflect.Type:用于操作类信息,类信息是只能读取
reflect.Type 可以通过 reflect.Value 得到,但是反过来则不行。
1.3 Kind 方法
reflect 包有一个很强的假设:你知道你操作的是什么 Kind(类型)。
Kind:Kind 是一个枚举值,用来判断操作的对应类型,例如是否是指针、是否是数组、是否是切片等。所以 reflect 的方法,如果你调用得不对,它直接就 panic。
例如在 reflect.Type 这里,这三个方法都有对应的 Kind 必须是什么,否则panic。
在调用 API 之前一定要先读注释,确认什么样的情况下可以调用!
代码演示
反射输出所有的字段名字,关键点在于,只有 Kind== Struct 的才有字段。而指针类型(Kind== Ptr)是没有的!
还要考虑:
- 输入是不是指针,会不会是多重指针
- 输入会不会是数组或者切片
- 结构体字段会不会也是结构体
(1)用反射输出字段名字和值
// IterateFields 返回所有的字段名字
// val 只能是结构体,或者结构体指针,可以是多重指针
func IterateFields(input any) (map[string]any, error) {
if input == nil {
return nil, errors.New("输入对象不能为空")
}
typ := reflect.TypeOf(input)
val := reflect.ValueOf(input)
// 处理指针,要拿到指针指向的东西
// 这里我们综合考虑了多重指针的效果
for typ.Kind() == reflect.Ptr {
typ = typ.Elem()
val = val.Elem()
}
// 如果不是结构体,就返回 error
if typ.Kind() != reflect.Struct {
return nil, errors.New("非法类型")
}
num := typ.NumField()
res := make(map[string]any, num)
for i := 0; i < num; i++ {
field := typ.Field(i) // 字段信息
fieldVal := val.Field(i) // 字段的值信息
if field.IsExported() {
res[field.Name] = fieldVal.Interface()
} else {
// 为了演示效果,不公开字段我们用零值来填充
res[field.Name] = reflect.Zero(field.Type).Interface()
}
}
return res, nil
}
func TestIterateFields(t *testing.T) {
up := &types.User{Name: "queen"}
up2 := &up
testCases := []struct {
name string
input any
wantFields map[string]any
wantErr error
}{
{
// 空
name: "empty",
input: nil,
wantErr: errors.New("输入对象不能为空"),
},
{
// 普通结构体
name: "normal struct",
input: types.User{
Name: "Tom",
},
wantFields: map[string]any{
"Name": "Tom",
"age": 0,
},
},
{
// 指针
name: "pointer",
input: &types.User{
Name: "jack",
},
wantFields: map[string]any{
"Name": "jack",
"age": 0,
},
},
{
// 多重指针
name: "multiple pointer",
input: up2,
wantFields: map[string]any{
"Name": "queen",
"age": 0,
},
},
{
// 非法输入
name: "slice",
input: []string{},
wantErr: errors.New("非法类型"),
},
{
// 非法指针输入
name: "pointer to map",
input: &(map[string]any{}),
wantErr: errors.New("非法类型"),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
res, err := IterateFields(tc.input)
assert.Equal(t, tc.wantErr, err)
if err != nil {
return
}
assert.Equal(t, tc.wantFields, res)
})
}
}
复制代码
要注意字段的作用域问题。反射能够拿到私有字段的类型信息,但是拿不到值。
1.4 指针和指针指向的结构体
指针(pointer)
指针,指向目标的内存地址。许多在Python
这样的动态类型语言中实现的效果,例如方法,在Go
中就需要引用指针来完成。
&
操作符可以生成一个指针。例如——
i := 42
p := &i
复制代码
这里p
就是i
的指针。
需要注意的是*
操作符,它跟在type
和pointer
前面的效果是不一样的。*type
代表接受或者返回的是一个指针,声明函数的时候经常用到。*pointer
代表通过指针pointer
取其对应的值。
i := 42
p := &i
*p = 420
// i = 420
复制代码
上面的代码中,p
是i
的指针,通过*p
操作相当于直接对i
重新赋值,所以i
会变成420
。而下面这个函数接受一个指针p
,然后也是通过*p
操作目标。
func operPointer(p *int) {
*p = *p * 100
// 接受的参数是个指针
// 函数内通过*pointer设置指针所指向的值
// *Type代表接受一个指针;
// 而*Pointer却代表操作指针所指向的值
}
复制代码
结构体 & 指针
一般来说,通过指针访问一个对象的方式是*pointer
,假设有一个指针指向一个结构体,按照这个逻辑,访问结构体字段的方式就应该是(*pointer).field
,但是Go
对结构体的指针做了优化,我们可以直接通过pointer.field
的方式,用指针访问其指向的结构体的字段。所以就可以有下面这样的函数了。
func Scale5P(p *MyNumbers) {
p.X = p.X * 5
p.Y = p.Y * 5
//可以通过指针直接访问值
//Go编译器翻译为了(*i).X和(*i).Y
}
复制代码
函数Scale5P()
接受一个类型为MyNumbers
的指针,但是在函数内,我们可以直接通过指针p
来访问结构体的字段。
func PointerKey3() {
i := MyNumbers{1, 2}
// Scale5P(i)
Scale5P(&i)
// 而对于函数Scale5P()来说, 由于接受的参数是个指针, 所以必须传入&i
fmt.Println(i)
}
复制代码
代码演示
(1) 用反射设置
反射可以被用来修改一个字段的值。但修改字段的值之前一定要先检查 CanSet (该字段是否能被修改)
简单来说,就是必须使用结构体指针, 那么结构体的字段才是可以修改的。 当然指针指向的对象也是可以修改的。
func SetField(entity any, field string, newVal any) error {
if entity == nil {
return errors.New("实体不能为nil")
}
if field == "" {
return errors.New("字段不能为空字符串")
}
val := reflect.ValueOf(entity)
typ := val.Type()
if typ.Kind() != reflect.Ptr || typ.Elem().Kind() != reflect.Struct {
return errors.New("非法类型")
}
typ = typ.Elem()
val = val.Elem()
fd := val.FieldByName(field)
if _, found := typ.FieldByName(field); !found {
return errors.New("字段不存在")
}
if !fd.CanSet() { // 判断字典是否可修改
return errors.New("不可修改字段")
}
// 为字段设置值
fd.Set(reflect.ValueOf(newVal))
return nil
}
func TestSetField(t *testing.T) {
testCases := []struct {
name string
field string
entity any
newVal any
wantErr error
}{
{
name: "empty",
field: "Name",
entity: nil,
wantErr: errors.New("实体不能为nil"),
},
{
name: "struct",
field: "Name",
entity: types.User{},
wantErr: errors.New("非法类型"),
},
{
name: "private field",
field: "age",
entity: &types.User{},
wantErr: errors.New("不可修改字段"),
},
{
name: "invalid field",
entity: &types.User{},
field: "invalid_field",
wantErr: errors.New("字段不存在"),
},
{
name: "pass",
field: "Name",
entity: &types.User{},
newVal: "Tom",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := SetField(tc.entity, tc.field, tc.newVal)
assert.Equal(t, tc.wantErr, err)
})
}
}
复制代码
(2) 输出方法信息并且执行调用
输出:
- 方法名
- 方法参数
- 返回值
注意:即便把字段定义为函数类型,它依然被算作字段,而不是方法。 所以严格来说,我们这里应该叫做输出函数类型的字段的信息,并且执行。
// IterateFuncs 输出方法信息,并执行调用
func IterateFuncs(val any) (map[string]*FuncInfo, error) {
typ := reflect.TypeOf(val)
if typ.Kind() != reflect.Struct || typ.Kind() != reflect.Ptr {
return nil, errors.New("非法类型")
}
num := typ.NumMethod()
result := make(map[string]*FuncInfo, num)
for i := 0; i < num; i++ {
f := typ.Method(i) // 通过 下标 获得 方法
numIn := f.Type.NumIn() // 获得方法的参数数量
ps := make([]reflect.Value, 0, numIn)
// 第一个参数永远都是接收器,类似于 java 的 this, python 的self 概念
ps = append(ps, reflect.ValueOf(val))
in := make([]reflect.Type, 0, numIn)
for j := 0; j < numIn; j++ {
p := f.Type.In(j) // 获得方法的输入参数的类型
in = append(in, p)
if j > 0 {
// 将 p 类型参数的 0值 加入 ps
ps = append(ps, reflect.Zero(p))
}
}
// 调用结果
ret := f.Func.Call(ps)
outNum := f.Type.NumOut() // 获得方法输出的结果数量
out := make([]reflect.Type, 0, outNum)
res := make([]any, 0, outNum)
for k := 0; k < outNum; k++ {
out = append(out, f.Type.Out(k)) // f.Type.Out(k) 获得方法的输出参数类型 将输出的参数类型加入到 out
res = append(res, ret[k].Interface()) // ret[k].Interface() 获得输出结果 将调用的结果加入到 res
}
result[f.Name] = &FuncInfo{
Name: f.Name,
In: in,
Out: out,
Result: res,
}
}
return result, nil
}
type FuncInfo struct {
Name string
In []reflect.Type
Out []reflect.Type
// 反射调用得到的结果
Result []any
}
func TestIterateFuncs(t *testing.T) {
testCases := []struct {
name string
input any
wantRes map[string]*FuncInfo
wantErr error
}{
{
// 普通结构体
name: "normal struct",
input: types.User{},
wantRes: map[string]*FuncInfo{
"GetAge": {
Name: "GetAge",
In: []reflect.Type{reflect.TypeOf(types.User{})},
Out: []reflect.Type{reflect.TypeOf(0)},
Result: []any{0},
},
},
},
{
// 指针
name: "pointer",
input: &types.User{},
wantRes: map[string]*FuncInfo{
"GetAge": {
Name: "GetAge",
In: []reflect.Type{reflect.TypeOf(&types.User{})},
Out: []reflect.Type{reflect.TypeOf(0)},
Result: []any{0},
},
"ChangeName": {
Name: "ChangeName",
In: []reflect.Type{reflect.TypeOf(&types.User{}), reflect.TypeOf("")},
Out: []reflect.Type{},
Result: []any{},
},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
res, err := IterateFuncs(tc.input)
assert.Equal(t, tc.wantErr, err)
if err != nil {
return
}
assert.Equal(t, tc.wantRes, res)
})
}
}
复制代码
注意事项:
-
方法接收器
- 以结构体作为输入,那么只能访问到结构体作为接收器的方法
- 以指针作为输入,那么能访问到任何接收器的方法
-
输入的第一个参数,永远都是接收器本身
(3) 为什么不能修改方法实现
因为go runtime 没暴露接口
(4) 反射遍历
遍历:
- 数组
- 切片
- 字符串
- map (Map 的遍历和其它三个不同)
// Iterate 迭代数组,切片,或者字符串
func IterateArray(input any) ([]any, error) {
val := reflect.ValueOf(input)
typ := val.Type()
kind := typ.Kind()
if kind != reflect.Array && kind != reflect.Slice && kind != reflect.String {
return nil, errors.New("非法类型")
}
res := make([]any, 0, val.Len())
for i := 0; i < val.Len(); i++ {
ele := val.Index(i)
res = append(res, ele.Interface())
}
return res, nil
}
// IterateMapV1 返回键,值
func IterateMapV1(input any) ([]any, []any, error) {
val := reflect.ValueOf(input)
if val.Kind() != reflect.Map {
return nil, nil, errors.New("非法类型")
}
l := val.Len()
keys := make([]any, 0, l)
values := make([]any, 0, l)
for _, k := range val.MapKeys() {
keys = append(keys, k.Interface())
v := val.MapIndex(k)
values = append(values, v.Interface())
}
return keys, values, nil
}
// IterateMapV2 返回键,值
func IterateMapV2(input any) ([]any, []any, error) {
val := reflect.ValueOf(input)
if val.Kind() != reflect.Map {
return nil, nil, errors.New("非法类型")
}
l := val.Len()
keys := make([]any, 0, l)
values := make([]any, 0, l)
itr := val.MapRange()
for itr.Next() {
keys = append(keys, itr.Key().Interface())
values = append(values, itr.Key().Interface())
}
return keys, values, nil
}
复制代码
只需要调用这些 value 上的 Set 方法就 可以修改这些值,注意检查 CanSet。
1.5 开源实例
(1) Dubbo-go 反射生成代理
在 Go 里面,生成代理的机制稍微有点奇怪。因为 Go 并没有提供篡改方法实现的 API,所以实际上我们是声明一个方法类型的字段。
这一个生成代理机制,是设计 RPC 框架的核心环节。
makeDubboCallProxy 就是我们的新值,用来取代旧的。
(2) Beego 反射解析模型数据
在 ORM 框架里面,一个很重要的环境是解析模型定义,比如说用户定义一个User,要从里面解析出来表名、列名、主键、外键、索引、关联关系等。
Beego 的这部分工作在 orm 包下面的modelCache
的 register 方法里面完成。
解析模型元数据这一步虽然很重要,但是不需要纠缠细节。因为不同的人设计 ORM 框架,模型定 义规范都是不同的。但是这些 ORM 框架需要的元数据总是类似的,总结出来这一点就可以。
(3) GORM 反射解析模型数据
GORM 也是类似,它将模型元数据称为Schema。核心代码在ParseWithSpecialTableName
中。 , 下边是入口,一大堆的校验揭示了 GORM支持什么样的模型定义。
逐个字段解析,可以清晰看到,它只解析公开字段。
GORM 利用 tag 来允许用户设置一些对字段的描述,例如是否是主键、是否允许自增。
官网定义的模型,使用了 tag (标签)。
ParseField是根据 GORM 的设计来的。
(4) Beego 与 GORM 模型元数据的对比
表名、列名、主键、外键、索引、关联关系。核心是学习这两个框架是怎么表达这些信息,也就是modelInfo 和 Schema 两个结构体的定义。
1.6 Go 反射编程小技巧
- 读写值,使用 reflect.Value
- 读取类型信息,使用 reflect.Type
- 时刻注意你现在操作的类型是不是指针。指针和指针指向的对象在反射层面上是两个东西
- 大多数情况,指针类型对应的 reflect.Type 毫无用处。我们操作的都是指针指向的那个类型
- 没有足够的测试就不要用反射,因为反射 API 充斥着 panic
- 切片和数组在反射上也是两个东西
- 方法类型的字段和方法,在反射上也是两个不同的东西
1.7 Go 反射面试要点
- Go 反射面得很少,因为 Go 反射本身是一个写代码用的,理论上的东西不太多。
- 什么是反射?反射可以看做是对对象和对类型的描述,而我们可以通过反射来间接操作对象。
- 反射的使用场景?一大堆,基本上任何高级框架都会用反射,ORM 是一个典型例子。Beego 的 controller 模式的 Web 框架也利用了反射。
- 能不能通过反射修改方法?不能。为什么不能?go runtime 没暴露接口。
- 什么样的字段可以被反射修改?有一个方法 CanSet 可以判断,简单来说就是addressable。