Go反射

103 阅读4分钟

Go反射

反射

反射是指在程序运行期对程序本身进行访问和修改的能力。程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。

支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。

Go程序在运行期使用reflect包访问程序的反射信息。

注意:Go中反射不能获取和修改 私有的属性以及方法

优缺点

  • 优点

    • 反射可以增加程序的灵活性,避免将程序写死到代码里
    • 代码简洁,提高代码的复用率,外部调用方便
    • 对于任意一个类(or go的结构体),都能够知道这个类(or go的结构体)的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法。
  • 缺点

    • 使用反射比正常代码性能低。可以进行优化。
    • 使用反射会模糊程序内部逻辑,使代码更复杂。
    • 会破坏私密性。不过在Go中反射不能获取和修改 私有的属性以及方法。

reflect包

Go语言中的反射是由 reflect 包提供支持的,它定义了两个重要的类型 Type 和 Value。

任意接口值在反射中都可以理解为由 reflect.Type 和 reflect.Value 两部分组成,并且 reflect 包提供了 reflect.TypeOf 和 reflect.ValueOf 两个函数来获取任意对象的 Value(对象的信息) 和 Type(对象的类型的信息)。

reflect中Type与Kind区别:Kind用于表示某一类(比如结构体类struct,指针类ptr),Type用于表示某一类中的具体类型(A{},*A)

基于实践学习

  • 获取结构体标签

    • 通过reflect.TypeOf()获取type,然后再通过type的Field相关方法获取结构体对应的字段信息structField,然后再通过structField的Tag方法获取该字段对应的tag,然后再调用tag的Get方法获取指定的标签
    • type A struct {
          Name string `thy:"mobile;max:11;min:6"` // 一般通过" "来区分不同类的tag,通过";"来区别同类tag的不同标志
      }
      aIns := &A{Name: "test"}
      reflect.TypeOf(aIns).FieldByName("Name").Tag.Get("thy") // mobile;max:11;min:6
      // 后续通过;区分开mobile,max:11min:6,然后根据不同的标志调用不同方法去验证值
      
    • 代码示例:github.com/tianheyi/go…
  • 通过类型动态创建实例

    • 使用reflect.New(typ Type)或者reflect.NewAt(typ Type, p unsafe.Pointer) 方法创建对应类型的实例,会返回对应类型实例(为reflect.Value类型)
    • 如果需要将reflect.Value类型的实例转换成对应类型的实例,可通过reflect.Value对象调用Interface()方法将其转化为interface{}类型,然后通过类型断言来转化为实际类型的实例。
  • 通过函数名调用方法

    • 调用普通方法

      func Add(x, y int) int {
          return x + y
      }
      func main() {
          funcV := reflect.ValueOf(Add) // 将Add方法转化为reflect.Value类型
          result := funcV.Call([]reflect.Value{reflect.ValueOf(3), reflect.ValueOf(2)}) // 调用该方法,并且传入两个参数3和2,对应形参中的x,y
          fmt.Println(result[0].Int()) // 获取第一个返回值,以int类型打印
      }
      
    • 调用某对象方法

      type A struct { 
          Name string 
      }
      func (a A) SetName1(name string) { a.Name = name }
      func (a *A) SetName2(name string) { a.Name = name }
      func (a A) GetName1() string { return a.Name }
      func (a *A) GetName2() string { return a.Name }
      func main() {
          // 直接调用
          aIns := &A{Name: "thy"}
          aIns.SetName1("thy1")
          fmt.Println(aIns.GetName1(),aIns.GetName2())
          aIns.SetName2("thy1")
          fmt.Println(aIns.GetName1(),aIns.GetName2())
          
          // 通过反射包调用
          aIns = &A{Name: "thy"}
          v := reflect.ValueOf(aIns)
          fmt.Println(v.Kind(), v.Elem().Kind())
          
          v.Elem().MethodByName("SetName1").Call([]reflect.Value{reflect.ValueOf("thy1")})
          result1 := v.MethodByName("GetName1").Call([]reflect.Value{})
          result2 := v.MethodByName("GetName2").Call([]reflect.Value{})
          fmt.Println(result1[0].String(), result2[0].String())
      ​
          v.MethodByName("SetName2").Call([]reflect.Value{reflect.ValueOf("thy1")})
          result1 = v.MethodByName("GetName1").Call([]reflect.Value{})
          result2 = v.MethodByName("GetName2").Call([]reflect.Value{})
          fmt.Println(result1[0].String(), result2[0].String())
      }
      

      打印结果:

      thy thy
      thy1 thy1 
      ptr struct
      thy thy   
      thy1 thy1 
      

      通过打印结果可知,无论是直接调用还是通过反射包调用,方法SetName1都没有修改变量成功。

      方法SetName1修改变量并没有成功,而SetName2修改变量成功。并且在

      • 直接调用方法时,用指针类型(pointer)与非指针类型(value)实例调用方法(无论是修改实例的方法还是与修改实例无关的方法)时不受方法集的约束,因为编译器总是去查找全部方法,并且自动转换调用者类型。比如:

        aIns := &A{Name: "thy"}
        aIns.SetName1("thy1") // 编译器会自动转换成(*aIns).SetName1("thy1")
        
      • 反射包调用方法时,当调用修改实例的方法(SetName1、SetName2)时,只有非指针类型(A)才能调用非指针方法(SetName1),指针类型(*A)才能调用指针方法(SetName2),不然会panic。而调用非修改实例的方法(GetName1、GetName2)时,非指针类型和指针类型都可以调用而不会panic。所以可以看出,当使用反射调用方法时,不全是查找全部方法,比如当调用修改实例的方法时。