什么是反射?

Java言语相同,Go言语也有运转时反射,这为咱们供给了一种能够在运转时操作任意类型目标的才能。比方查看一个接口变量的详细类型、看看一个结构体有多少字段、修正某个字段的值等。Go言语是静态编译类言语,比方在界说一个变量的时候,已经知道了它是什么类型,那么为什么还需求反射呢?这是由于有些工作只有在运转时才知道。比方你界说了一个函数,它有一个interface{}类型的参数,这也就意味着调用者能够传递任何类型的参数给这个函数。在这种情况下,假如你想知道调用者传递的是什么类型的参数,就需求用到反射。假如你想知道一个结构体有哪些字段和办法,也需求反射。

获取目标的值和类型

Go言语的反射界说中,任何接口都由两部分组成:接口的详细类型,以及详细类型对应的值。比方var i int=3,由于interface{}能够表明任何类型,所以变量i能够转为interface{}。你能够把变量i当成一个接口,那么这个变量在Go反射中的表明便是<Value,Type>。其间Value为变量的值,即3,而Type为变量的类型,即int

提示:

interface{}是空接口,能够表明任何类型,也便是说你能够把任何类型转换为空接口,它一般用于反射、类型断言,以削减重复代码,简化编程。

Go反射中,规范库为咱们供给了两种类型reflect.Valuereflect.Type来别离表明变量的值和类型,而且供给了两个函数reflect.ValueOfreflect.TypeOf别离获取任意目标的reflect.Valuereflect.Type

package main
import(
    "fmt"
    "reflect"
)
func main(){
    i := 3
    iv := reflect.ValueOf(i)
    it := reflect.TypeOf(i)
    fmt.Println(iv, it)
    // 3  int
}

代码界说了一个int类型的变量i,它的值为3,然后经过reflect.ValueOfreflect.TypeOf函数就能够取得变量i对应的reflect.Valuereflect.Type。经过fmt.Println函数打印后,能够看到成果是“3 int”,这也能够证明reflect.Value表明的是变量的值,reflect.Type表明的是变量的类型。

reflect.Value

reflect.Value能够经过函数reflect.ValueOf取得,下面将为你介绍它的结构和用法。

reflect.Value 结构体界说

Go言语中,reflect.Value被界说为一个结构体,它的界说如下面的代码所示:

type Value struct {
    //typ_ 保存由 value 表明的值的类型。
    //运用 typ 办法拜访以避免 v 的转义。
    typ_ *abi.Type
    //指针值数据,或许,假如设置了 flagIndir,则为指向数据的指针。
    //当 flagIndir 被设置或 typ.pointers()为true时有用。
    ptr unsafe.Pointer
    //标志保存有关该值的元数据
    //最低的五位给出值的种类,镜像类型。Kind()。
    //下一组位是标志位:
    //-flagStickyRO:经过未过期未嵌入字段获取,因此为只读
    //-flagEmbedRO: 经过未导出的嵌入字段获取,因此为只读
    //-flagIndir:   val持有指向数据的指针
    //-flagAddr:    v.CanAddr为true(表明flagIndir和ptr为非零)
    //-flagMethod:  v是一个办法值。
    //假如ifaceIndir(typ),代码能够假定flagIndir已设置。
    //剩余的22+位给出了办法值的办法编号。
    //If flag.kind()!=Func,代码能够假定flagMethod是未设置的。
    flag
    //办法值表明当时的办法调用
    //相似r。读取某些接收器r。典型值+val+标志位描述
    //接收器r,但标志的Kind位表明Func(办法为
    //函数),而且标志的顶部位给出办法编号
    //在r的类型的办法表中。
}
type flag uintptr

咱们发现reflect.Value结构体的字段都是私有的,也便是说,咱们只能运用reflect.Value的办法。现在看看它有哪些常用办法,如下所示:

  • 针对详细类型的系列办法
// 用户获取对应的值
Bool()
Bytes()
Complex()
Float()
Int()
String()
Uint()
CanAddr()  // 是否能够运用Addr获取值的地址
CanSet()   // 是否能够修正对应的值
// 用户修正对应的值
Set()
SetBool()
SetBytes()
SetComplex()
SetFloat()
SetInt()
SetString()
Elem() // 获取指针指向的值,一般用于修正对应的值
  • 针对 struct 类型的系列办法
// Field 系列办法用于获取 struct 类型中的字段
Filed()
FiledByIndex()
FiledByName()
FiledByNameFunc()
Interface() // 获取对应的原始类型
IsNil()     // 值是否为nil
IsZero()    // 值是否是零值
Kind()      // 获取对应的类型类别,比方 Array、Slice、Map 等
  • 获取类型上的办法集
// 获取对应的办法
Method()
MethodByName()
NumField()   //获取 struct 类型中字段的数量
NumMethod()  // 获取类型上办法集的数量
Type()       // 获取对应的 reflect.Type

看着比较多,其实就三类:

  • 一类用于获取和修正对应的值;
  • 一类与struct类型的字段有关,用于获取对应的字段;
  • 一类与类型上的办法集有关,用于获取对应的办法。

reflect.Type

reflect.Value能够用于与值有关的操作,而假如是与变量类型自身有关的操作,比方要获取结构体对应的字段名称或办法,则最好运用reflect.Type

reflect.Type 接口界说

reflect.Value不同,reflect.Type是一个接口,而不是一个结构体,所以也只能运用它的办法。

type Type interface {
    // 回来值的对齐办法(以字节为单位)
    // 用作在内存中分配时
    Align() int
    // 回来值的对齐办法(以字节为单位)
    // 用作结构中的字段
    FieldAlign() int
    // 回来已界说类型的包中的类型名称
    Name() string
    // 回来界说类型的包途径,即导入途径
    PkgPath() string
    // 回来存储所需的字节数
    Size() uintptr
    // 回来该类型的字符串表明方式
    String() string
    Implements(u Type) bool
    AssignableTo(u Type) bool
    ConvertibleTo(u Type) bool
    Comparable() bool
    // 以位为单位回来类型的巨细
    Bits() int
    // 回来通道类型的方向
    ChanDir() ChanDir
    // 陈述函数类型的最终输入参数
    IsVariadic() bool
    // 回来函数类型的第i个输入参数的类型
    In(i int) Type
    // 回来映射类型的键类型
    Key() Type
    // 回来数组类型的长度
    Len() int
    // 回来函数类型的输入参数计数
    NumIn() int
    // 回来函数类型的输出参数计数
    NumOut() int
    // 回来函数类型的第i个输出参数的类型
    Out(i int) Type
    // 以下这些办法与 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
}

其间有几个特有的办法:

  • Implements 用于判别是否完成了接口 u
  • AssignableTo 用于判别是否能够运用“=”,即赋值运算符赋值给类型 u
  • ConvertibleTo 用于判别是否能够转换成类型 u,其实便是是否能够进行类型转换
  • Comparable 用于判别该类型是否能够运用联系运算符进行比较

小技巧

你能够经过FieldByName办法获取指定的字段,也能够经过MethodByName办法获取指定的办法,这在需求获取某个特定的字段或许办法而不是遍历时十分高效。

是否完成某接口?

经过 reflect.Type 中的Implements 能够判别是否完成了某接口。

package main
import (
    "fmt"
    "io"
    "reflect"
)
type persion struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}
// 为 persion 增加一个办法 String ,回来对应的字符串信息
// 这样 persion 结构体就完成了 fmt.Stringer 接口
func (p persion) String() string {
    return fmt.Sprintf("Name is %s, Age is %d", p.Name, p.Age)
}
func main() {
    p := persion{
       Name: "码一行",
       Age:  26,
    }
    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))
}

提示

尽可能经过类型断言的办法判别是否完成了某接口,而不是经过反射

这个示例经过Implements办法来判别是否完成了fmt.Stringerio.Writer接口, 运转成果:

是否完成了 fmt.Stringer:  true
是否完成了 io.Writer:  false

由于结构体person只完成了fmt.Stringer接口,没有完成io.Writer接口,所以与验证的成果一致。

反射规律

反射是计算机言语中程序检视其自身结构的一种办法,它属于元编程的一种方式。反射灵活、强壮,但也存在不安全要素。它能够绕过编译器的许多静态查看,假如过多运用便会造成紊乱。为了帮助开发者更好地了解反射,Go言语的作者在博客上总结了反射的三大规律。

  1. 任何接口值interface{}都能够反射出反射目标,也便是reflect.Valuereflect.Type经过函数reflect.ValueOfreflect.TypeOf取得。
  2. 反射目标也能够还原为interface{}变量,也便是第1条规律可逆性,经过reflect.Value结构体的Interface办法取得。
  3. 要修正反射的目标,该值有必要可设置,也便是可寻址

提示

任何类型的变量都能够转换为空接口intferface{}

所以第1条规律中函数reflect.ValueOfreflect.TypeOf的参数便是interface{},表明能够把任何类型的变量转换为反射目标。

在第2条规律中,reflect.Value结构体的Interface办法回来的值也是interface{},表明能够把反射目标还原为对应的类型变量。

一旦你了解了这三大规律,就能够更好地了解和运用Go言语反射。

结束语

在反射中,reflect.Value对应的是变量的值,假如你需求进行与变量的值有关的操作,应该优先运用reflect.Value,比方获取变量的值、修正变量的值等。reflect.Type对应的是变量的类型,假如你需求进行与变量的类型自身有关的操作,应该优先运用reflect.Type,比方获取结构体内的字段、类型拥有的办法集等。

此外我要再次着重:反射尽管很强壮,能够简化编程、削减重复代码,但是过度运用会让你的代码变得复杂紊乱。所以除非十分必要,不然尽可能少地运用它们。