Go语言(运行时反射,深度解析!)

我们在开发的时候经常会遇到字符串跟结构体之间的转换,比如在调用 API 时,需要将 JSON 字符串转成 struct 结构体。具体如何进行转换,就需要用到反射了。
反射 对于反射,之前的文章已经有所介绍,传送门:《断言、反射的理解与使用!》,此处我们再深入讲解下反射。
reflect.Value 和 reflect.Type 反射有两种类型 reflect.Valuereflect.Type ,分别表示变量的值和类型,并且提供了两个函数 reflect.ValueOfreflect.TypeOf 分别获取任意对象的 reflect.Valuereflect.Type
reflect.Value reflect.Value 可以通过函数 reflect.ValueOf 获得。reflect.Value 被定义为一个 struct 结构体:

type Value struct { typ *rtype ptr unsafe.Pointer flag }

可以看到,这个结构体中的字段都是私有的,我们只能使用 reflect.Value 的方法,它的方法有:
//针对具体类型的系列方法 //以下是用于获取对应的值 Bool Bytes Complex Float Int String Uint CanSet //是否可以修改对应的值 //以下是用于修改对应的值 Set SetBool SetBytes SetComplex SetFloat SetInt SetString Elem //获取指针指向的值,一般用于修改对应的值 //以下Field系列方法用于获取struct类型中的字段 Field FieldByIndex FieldByName FieldByNameFunc Interface //获取对应的原始类型 IsNil //值是否为nil IsZero //值是否是零值 Kind //获取对应的类型类别,比如Array、Slice、Map等 //获取对应的方法 Method MethodByName NumField //获取struct类型中字段的数量 NumMethod//类型上方法集的数量 Type//获取对应的reflect.Type

方法分为三类:
  1. 获取和修改对应的值
  2. struct 类型的字段有关,用于获取对应的字段
  3. 类型上的方法集有关,用于获取对应的方法
任意类型的对象 与 reflect.Value 互转:
func main() { i := 5 //int to reflect.Value iv:=reflect.ValueOf(i) //reflect.Value to int i1 := iv.Interface().(int) fmt.Println(i1) }

修改对应的值
func main() { i := 5 ipv := reflect.ValueOf(&i) ipv.Elem().SetInt(6) fmt.Println(i) }

  • 示例中我们通过反射修改了一个变量
  • reflect.ValueOf 函数返回的是一份值的拷贝,所以我们要传入变量的指针才可以
  • 因为传递的是一个指针,所以需要调用 Elem 方法找到这个指针指向的值,这样才能修改
  • 要修改一个变量的值,关键点:传递指针(可寻址),通过 Elem 方法获取指向的值,才可以保证值可以被修改,reflect.Value 为我们提供了 CanSet 方法判断是否可以修改该变量
修改 struct 结构体字段的值
func main() { p := person{Name: "微客鸟窝",Age: 18} pv:=reflect.ValueOf(&p) pv.Elem().Field(0).SetString("无尘") fmt.Println(p) } type person struct { Name string Age int }

步骤总结:
  1. 传递一个 struct 结构体的指针,获取对应的 reflect.Value;
  2. 通过 Elem 方法获取指针指向的值;
  3. 通过 Field 方法获取要修改的字段;
  4. 通过 Set 系列方法修改成对应的值。
    通过反射修改一个值的规则:
    • 可被寻址,通俗地讲就是要向 reflect.ValueOf 函数传递一个指针作为参数。
    • 如果要修改 struct 结构体字段值的话,该字段需要是可导出的,而不是私有的,也就是该字段的首字母为大写。
    • 记得使用 Elem 方法获得指针指向的值,这样才能调用 Set 系列方法进行修改。
获取对应的底层类型
因为我们可以通过 type 关键字来自定义一些新的类型,而底层类型就是一些基础类型。比如上面示例中的 p 变量类型为 person,底层类型为 struct 结构体类型。
func main() { p := person{Name: "微客鸟窝",Age: 18} pv:=reflect.ValueOf(&p) fmt.Println(pv.Kind()) pv:=reflect.ValueOf(p) fmt.Println(pv.Kind()) }//运行结果: ptr struct

Kind 方法返回一个 Kind 类型的值,它是一个常量,从源码看下定义的 Kind 常量列表,含了 Go 语言的所有底层类型:
type Kind uint const ( Invalid Kind = iota Bool Int Int8 Int16 Int32 Int64 Uint Uint8 Uint16 Uint32 Uint64 Uintptr Float32 Float64 Complex64 Complex128 Array Chan Func Interface Map Ptr Slice String Struct UnsafePointer )

reflect.Type
  • reflect.Type 是一个接口,而不是一个结构体,所以也只能使用它的方法。
  • reflect.Type 接口常用的方法
    type Type interface { Implements(u Type) bool //方法用于判断是否实现了接口 u; AssignableTo(u Type) bool //方法用于判断是否可以赋值给类型 u,其实就是是否可以使用 =,即赋值运算符; ConvertibleTo(u Type) bool //方法用于判断是否可以转换成类型 u,其实就是是否可以进行类型转换; Comparable() bool //方法用于判断该类型是否是可比较的,其实就是是否可以使用关系运算符进行比较。 //以下这些方法和Value结构体的功能相同 Kind() Kind Method(int) Method MethodByName(string) (Method, bool) NumMethod() int Elem() Type Field(i int) StructField FieldByIndex(index []int) StructField FieldByName(name string) (StructField, bool) FieldByNameFunc(match func(string) bool) (StructField, bool) NumField() int }

遍历结构体的字段和方法
package mainimport ( "fmt" "reflect" )func main() { p := person{Name: "微客鸟窝", Age: 18} pt := reflect.TypeOf(p) //遍历person的字段 for i := 0; i < pt.NumField(); i++ { fmt.Println("字段:", pt.Field(i).Name) } //遍历person的方法 for i := 0; i < pt.NumMethod(); i++ { fmt.Println("方法:", pt.Method(i).Name) } }type person struct { Name string Ageint }func (p person) String() string{ return fmt.Sprintf("Name is %s,Age is %d",p.Name,p.Age) }

运行结果:
字段: Name 字段: Age 方法: String

是否实现某接口
//.... func main() { p := person{Name: "微客鸟窝", Age: 20} pt := reflect.TypeOf(p) stringerType := reflect.TypeOf((*fmt.Stringer)(nil)).Elem() writerType := reflect.TypeOf((*io.Writer)(nil)).Elem() fmt.Println("是否实现了fmt.Stringer:", pt.Implements(stringerType)) fmt.Println("是否实现了io.Writer:", pt.Implements(writerType)) } //...

运行结果:
是否实现了fmt.Stringer: true 是否实现了io.Writer: false

JSON 和 Struct 互转 我们可以通过标准库中的 json 包进行JSON 和 Struct 互转:
//...... func main() { p := person{Name: "微客鸟窝", Age: 18} //struct to json jsonB, err := json.Marshal(p) if err == nil { fmt.Println(string(jsonB)) } //json to struct respJSON := "{\"Name\":\"无尘\",\"Age\":18}" json.Unmarshal([]byte(respJSON), &p) fmt.Println(p) } //......

运行结果:
{"Name":"微客鸟窝","Age":18} Name is 无尘,Age is 18

  • 通过 json.Marshal 函数,可以把 struct 转为 JSON 字符串。
  • 通过 json.Unmarshal 函数,可以把 JSON 字符串转为 struct。
Struct Tag struct tag 是一个添加在 struct 字段上的标记,使用它进行辅助。要想获得字段上的 tag,就要先反射获得对应的字段,可以通过 Field 方法做到。该方法返回一个 StructField 结构体,它有一个字段是 Tag,存有字段的所有 tag。
示例:
//遍历person字段中key为json、bson的tag for i:=0; i

运行结果:
字段Name上,key为json的tag为name 字段Name上,key为bson的tag为b_name 字段Age上,key为json的tag为age 字段Age上,key为bson的tag为b_name

反射定律 【Go语言(运行时反射,深度解析!)】Go 语言的作者在博客上总结了反射的三大定律:
  1. 任何接口值 interface{} 都可以反射出反射对象,也就是 reflect.Value 和 reflect.Type,通过函数 reflect.ValueOf 和 reflect.TypeOf 获得。
  2. 反射对象也可以还原为 interface{} 变量,也就是第 1 条定律的可逆性,通过 reflect.Value 结构体的 Interface 方法获得。
  3. 要修改反射的对象,该值必须可设置,也就是可寻址,参考上节课修改变量的值那一节的内容理解。

    推荐阅读