下载APP

Go 反射

反射是指在程序运行期对程序本身进行访问和修改的能力,即可以在运行时动态获取变量的各种信息。Go的反射是通过接口的类型信息实现的,即反射建立在接口类型的基础上:当向接口变量赋予一个实体类型的时候,接口会存储实体的类型信息(Type)和值信息(Value)。因此我们可以在运行时利用反射

1.1 类型系统

绝大多数编程语言的类型系统都是类似的,会有声明类型、实际类型之类的分别。 image.png

在 Go 反射里面,一个实例可以看成两部分:

  • 实际类型

1.2 反射reflect的核心的两个API

  • reflect.Value:用于操作值,部分值是可以被反射修改的
  • reflect.Type:用于操作类信息,类信息是只能读取

image.png image.png

reflect.Type 可以通过 reflect.Value 得到,但是反过来则不行。

1.3 Kind 方法

reflect 包有一个很强的假设:你知道你操作的是什么 Kind(类型)。

Kind:Kind 是一个枚举值,用来判断操作的对应类型,例如是否是指针、是否是数组、是否是切片等。所以 reflect 的方法,如果你调用得不对,它直接就 panic。

例如在 reflect.Type 这里,这三个方法都有对应的 Kind 必须是什么,否则panic。

image.png

在调用 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的指针。

需要注意的是*操作符,它跟在typepointer前面的效果是不一样的。*type代表接受或者返回的是一个指针,声明函数的时候经常用到。*pointer代表通过指针pointer取其对应的值。

i := 42
p := &i
*p = 420
// i = 420
复制代码

上面的代码中,pi的指针,通过*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)
}
复制代码

image.png

代码演示

(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) 为什么不能修改方法实现

image.png

因为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,所以实际上我们是声明一个方法类型的字段。

image.png

这一个生成代理机制,是设计 RPC 框架的核心环节。

image.png

makeDubboCallProxy 就是我们的新值,用来取代旧的。

image.png

(2) Beego 反射解析模型数据

在 ORM 框架里面,一个很重要的环境是解析模型定义,比如说用户定义一个User,要从里面解析出来表名、列名、主键、外键、索引、关联关系等。

image.png

Beego 的这部分工作在 orm 包下面的modelCache 的 register 方法里面完成。

image.png

image.png

解析模型元数据这一步虽然很重要,但是不需要纠缠细节。因为不同的人设计 ORM 框架,模型定 义规范都是不同的。但是这些 ORM 框架需要的元数据总是类似的,总结出来这一点就可以。

(3) GORM 反射解析模型数据

GORM 也是类似,它将模型元数据称为Schema。核心代码在ParseWithSpecialTableName中。 , 下边是入口,一大堆的校验揭示了 GORM支持什么样的模型定义。

image.png

逐个字段解析,可以清晰看到,它只解析公开字段。 image.png

GORM 利用 tag 来允许用户设置一些对字段的描述,例如是否是主键、是否允许自增。

image.png

官网定义的模型,使用了 tag (标签)。 image.png

image.png

ParseField是根据 GORM 的设计来的。

(4) Beego 与 GORM 模型元数据的对比

image.png

image.png

image.png

表名、列名、主键、外键、索引、关联关系。核心是学习这两个框架是怎么表达这些信息,也就是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。
在线举报