乐趣区

关于golang: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 main

import (
    "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
    Age  int
}

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<pt.NumField();i++{sf:=pt.Field(i)
   fmt.Printf("字段 %s 上,json tag 为 %s\n",sf.Name,sf.Tag.Get("json"))
   fmt.Printf("字段 %s 上,bson tag 为 %s\n",sf.Name,sf.Tag.Get("bson"))
}
type person struct {
   Name string `json:"name" bson:"b_name"`
   Age int `json:"age" bson:"b_name"`
}

运行后果:

字段 Name 上,key 为 json 的 tag 为 name
字段 Name 上,key 为 bson 的 tag 为 b_name
字段 Age 上,key 为 json 的 tag 为 age
字段 Age 上,key 为 bson 的 tag 为 b_name

反射定律

Go 语言的作者在博客上总结了反射的三大定律:

  1. 任何接口值 interface{} 都能够反射出反射对象,也就是 reflect.Value 和 reflect.Type,通过函数 reflect.ValueOf 和 reflect.TypeOf 取得。
  2. 反射对象也能够还原为 interface{} 变量,也就是第 1 条定律的可逆性,通过 reflect.Value 构造体的 Interface 办法取得。
  3. 要批改反射的对象,该值必须可设置,也就是可寻址,参考上节课批改变量的值那一节的内容了解。
退出移动版