饶全成:深度解密 Go 语言之反射

出品 | 滴滴技术html

做者 | 饶成全程序员


前言:Go 做为一门静态语言,相比 Python 等动态语言,在编写过程当中灵活性会受到必定的限制。可是经过接口加反射实现了相似于动态语言的能力:能够在程序运行时动态地捕获甚至改变类型的信息和值。web

▍阅读索引编程


  • 什么是反射json

  • 为何要用反射数组

  • 反射是如何实现的浏览器

        types 和 interfacebash

        反射的基本函数服务器

        反射的三大定律网络

  • 反射相关函数的使用

        代码样例

        未导出成员

  • 反射的实际应用

         json 序列化

         DeepEqual 的做用及原理

  • 总结

  • 参考资料


▍什么是反射

直接看维基百科上的定义:

在计算机科学中,反射是指计算机程序在运行时(Run time)能够访问、检测和修改它自己状态或行为的一种能力。用比喻来讲,反射就是程序在运行的时候可以“观察”而且修改本身的行为。

那我就要问个问题了:不用反射就不能在运行时访问、检测和修改它自己的状态和行为吗?

问题的回答,其实要首先理解什么叫访问、检测和修改它自己状态或行为,它的本质是什么?

实际上,它的本质是程序在运行期探知对象的类型信息和内存结构,不用反射能行吗?能够的!使用汇编语言,直接和内层打交道,什么信息不能获取?可是,当编程迁移到高级语言上来以后,就不行了!就只能经过 反射来达到此项技能。

不一样语言的反射模型不尽相同,有些语言还不支持反射。《Go 语言圣经》中是这样定义反射的:

Go 语言提供了一种机制在运行时更新变量和检查它们的值、调用它们的方法,可是在编译时并不知道这些变量的具体类型,这称为反射机制。


▍为何要用反射

须要反射的 2 个常见场景:

一、有时你须要编写一个函数,可是并不知道传给你的参数类型是什么,多是没约定好;也多是传入的类型不少,这些类型并不能统一表示。这时反射就会用的上了。

二、有时候须要根据某些条件决定调用哪一个函数,好比根据用户的输入来决定。这时就须要对函数和函数的参数进行反射,在运行期间动态地执行函数。

在讲反射的原理以及如何用以前,仍是说几点不使用反射的理由:

一、与反射相关的代码,常常是难以阅读的。在软件工程中,代码可读性也是一个很是重要的指标。

二、Go 语言做为一门静态语言,编码过程当中,编译器能提早发现一些类型错误,可是对于反射代码是无能为力的。因此包含反射相关的代码,极可能会运行好久,才会出错,这时候常常是直接 panic,可能会形成严重的后果。

三、反射对性能影响仍是比较大的,比正常代码运行速度慢一到两个数量级。因此,对于一个项目中处于运行效率关键位置的代码,尽可能避免使用反射特性。

▍反射是如何实现的

interface是 Go 语言实现抽象的一个很是强大的工具。当向接口变量赋予一个实体类型的时候,接口会存储实体的类型信息,反射就是经过接口的类型信息实现的,反射创建在类型的基础上。

Go 语言在 reflect 包里定义了各类类型,实现了反射的各类函数,经过它们能够在运行时检测类型的信息、改变类型的值。

▍types 和 interface

Go 语言中,每一个变量都有一个静态类型,在编译阶段就肯定了的,好比 int,float64,[]int 等等。注意,这个类型是声明时候的类型,不是底层数据类型。

Go 官方博客里就举了一个例子

type MyInt int 

var i int
 
var j MyInt

复制代码

尽管 i,j 的底层类型都是 int,但咱们知道,他们是不一样的静态类型,除非进行类型转换,不然,i 和 j 不能同时出如今等号两侧。j 的静态类型就是 MyInt

反射主要与 interface{} 类型相关。前面一篇关于 interface 相关的文章已经探讨过 interface 的底层结构,这里再来复习一下。

type iface struct{

    tab  *itab

    data unsafe.Pointer

}

type itab struct{

    inter  *interfacetype

    _type  *_type

    link   *itab

    hash   uint32

    bad    bool

    inhash bool

    unused [2]byte

    fun [1]uintptr

}
复制代码

其中 itab 由具体类型 _type 以及 interfacetype 组成。 _type 表示具体类型,而 interfacetype 则表示具体类型实现的接口类型。


实际上,iface 描述的是非空接口,它包含方法;与之相对的是 eface,描述的是空接口,不包含任何方法,Go 语言里有的类型都 “实现了” 空接口。

type eface struct{

    _type *_type

    data unsafe.Pointer

}
复制代码

相比 iface, eface 就比较简单了。只维护了一个 _type 字段,表示空接口所承载的具体的实体类型。 data 描述了具体的值。


仍是用 Go 官方关于反射的博客里的例子,固然,我会用图形来详细解释,结合二者来看会更清楚。顺便提一下,搞技术的不要惧怕英文资料,要想成为技术专家,读英文原始资料是技术提升的一条必经之路。

先明确一点:接口变量能够存储任何实现了接口定义的全部方法的变量。

Go 语言中最多见的就是 Reader 和 Writer 接口:

type Readerinterface{

    Read(p []byte) (n int, err error)

}

type Writerinterface{

    Write(p []byte) (n int, err error)

}

复制代码

接下来,就是接口之间的各类转换和赋值了:

varr io.Reader

tty, err := os.OpenFile("/Users/qcrao/Desktop/test", os.O_RDWR, 0)

iferr != nil{

   returnnil, err

}

r = tty

复制代码

首先声明 r 的类型是 io.Reader,注意,这是 r 的静态类型,此时它的动态类型为 nil,而且它的动态值也是 nil。

以后, r=tty 这一语句,将 r 的动态类型变成 *os.File,动态值则变成非空,表示打开的文件对象。这时,r 能够用 <value,type>对来表示为: <tty, *os.File>。


注意看上图,此时虽然 fun 所指向的函数只有一个 Read 函数,其实 *os.File 还包含 Write 函数,也就是说 *os.File 其实还实现了 io.Writer 接口。

所以下面的断言语句能够执行:

varw io.Writer

w = r.(io.Writer)

复制代码

之因此用断言,而不能直接赋值,是由于 r 的静态类型是 io.Reader,并无实现 io.Writer 接口。断言可否成功,看 r 的动态类型是否符合要求。

这样,w 也能够表示成 <tty, *os.File>,仅管它和 r 同样,可是 w 可调用的函数取决于它的静态类型 io.Writer,也就是说它只能有这样的调用形式: w.Write() 。

w 的内存形式以下图:


和 r 相比,仅仅是 fun 对应的函数变了: Read->Write。

最后,再来一个赋值:

varempty interface{}

empty = w
复制代码

因为 empty 是一个空接口,所以全部的类型都实现了它,w 能够直接赋给它,不须要执行断言操做。



从上面的三张图能够看到,interface 包含三部分信息: _type 是类型信息, *data指向实际类型的实际值, itab 包含实际类型的信息,包括大小、包路径,还包含绑定在类型上的各类方法(图上没有画出方法)。

补充一下关于 os.File 结构体的图:



这一节的最后,复习一下上一篇关于 interface 的文章,提到的一个技巧,这里再展现一下:

先参考源码,分别定义一个 “假装”的 iface 和 eface 结构体。

type iface struct{

    tab  *itab

    data unsafe.Pointer

}

type itab struct{

    inter uintptr

    _type uintptr

    link uintptr

    hash  uint32

    _     [4]byte

    fun    [1]uintptr

}

type eface struct{

    _type uintptr

    data unsafe.Pointer

}

复制代码

接着,将接口变量占据的内存内容强制解释成上面定义的类型,再打印出来:

package main

import(

   "os"

   "fmt"

   "io"

   "unsafe"

)

func main() {

    varr io.Reader

    fmt.Printf("initial r: %T, %v\n", r, r)

    tty, _ := os.OpenFile("/Users/qcrao/Desktop/test", os.O_RDWR, 0)

    fmt.Printf("tty: %T, %v\n", tty, tty)

    // 给 r 赋值

    r = tty

    fmt.Printf("r: %T, %v\n", r, r)

    rIface := (*iface)(unsafe.Pointer(&r))

    fmt.Printf("r: iface.tab._type = %#x, iface.data = %#x\n", rIface.tab._type, rIface.data)

    // 给 w 赋值

    varw io.Writer

    w = r.(io.Writer)

    fmt.Printf("w: %T, %v\n", w, w)

    wIface := (*iface)(unsafe.Pointer(&w))

    fmt.Printf("w: iface.tab._type = %#x, iface.data = %#x\n", wIface.tab._type, wIface.data)

    // 给 empty 赋值

    varempty interface{}

    empty = w

    fmt.Printf("empty: %T, %v\n", empty, empty)

    emptyEface := (*eface)(unsafe.Pointer(&empty))

    fmt.Printf("empty: eface._type = %#x, eface.data = %#x\n", emptyEface._type, emptyEface.data)

}

复制代码

运行结果:

initial r: <nil>, <nil>

tty: *os.File, &{0xc4200820f0}

r: *os.File, &{0xc4200820f0}

r: iface.tab._type = 0x10bfcc0, iface.data = 0xc420080020

w: *os.File, &{0xc4200820f0}

w: iface.tab._type = 0x10bfcc0, iface.data = 0xc420080020

empty: *os.File, &{0xc4200820f0}

empty: eface._type = 0x10bfcc0, eface.data = 0xc420080020

复制代码

r,w,empty 的动态类型和动态值都同样。再也不详细解释了,结合前面的图能够看得很是清晰。

▍反射的基本函数

reflect 包里定义了一个接口和一个结构体,即 reflect.Type 和 reflect.Value,它们提供不少函数来获取存储在接口里的类型信息。

reflect.Type 主要提供关于类型相关的信息,因此它和 _type 关联比较紧密; reflect.Value 则结合 _type 和 data 二者,所以程序员能够获取甚至改变类型的值。

reflect 包中提供了两个基础的关于反射的函数来获取上述的接口和结构体:

func TypeOf(i interface{}) Type

func ValueOf(i interface{}) Value
复制代码

TypeOf 函数用来提取一个接口中值的类型信息。因为它的输入参数是一个空的 interface{},调用此函数时,实参会先被转化为 interface{} 类型。这样,实参的类型信息、方法集、值信息都存储到 interface{} 变量里了。

看下源码:

func TypeOf(i interface{}) Type{

    eface := *(*emptyInterface)(unsafe.Pointer(&i))

    returntoType(eface.typ)

}
复制代码

这里的 emptyInterface 和上面提到的 eface 是一回事(字段名略有差别,字段是相同的),且在不一样的源码包:前者在 reflect 包,后者在 runtime 包。 eface.typ 就是动态类型。

type emptyInterface struct{

    typ  *rtype

    word unsafe.Pointer

}
复制代码

至于 toType 函数,只是作了一个类型转换:

func toType(t *rtype) Type{

    ift == nil{

       return nil

    }
 
    returnt

}
复制代码

注意,返回值 Type 其实是一个接口,定义了不少方法,用来获取类型相关的各类信息,而 *rtype 实现了 Type 接口。

type Typeinterface{

     // 全部的类型均可以调用下面这些函数

     // 此类型的变量对齐后所占用的字节数

     Align() int

     // 若是是 struct 的字段,对齐后占用的字节数

     FieldAlign() int

     // 返回类型方法集里的第 `i` (传入的参数)个方法

     Method(int) Method

     // 经过名称获取方法

     MethodByName(string) (Method, bool)

     // 获取类型方法集里导出的方法个数

     NumMethod() int

     // 类型名称

     Name() string

     // 返回类型所在的路径,如:encoding/base64

     PkgPath() string

     // 返回类型的大小,和 unsafe.Sizeof 功能相似

     Size() uintptr

     // 返回类型的字符串表示形式

     String() string

     // 返回类型的类型值

     Kind() Kind

     // 类型是否实现了接口 u

     Implements(u Type) bool

     // 是否能够赋值给 u

     AssignableTo(u Type) bool

     // 是否能够类型转换成 u

     ConvertibleTo(u Type) bool

     // 类型是否能够比较

     Comparable() bool

     // 下面这些函数只有特定类型能够调用

     // 如:Key, Elem 两个方法就只能是 Map 类型才能调用

     // 类型所占据的位数

     Bits() int

     // 返回通道的方向,只能是 chan 类型调用

     ChanDir() ChanDir

     // 返回类型是不是可变参数,只能是 func 类型调用

     // 好比 t 是类型 func(x int, y ... float64)

     // 那么 t.IsVariadic() == true

     IsVariadic() bool

     // 返回内部子元素类型,只能由类型 Array, Chan, Map, Ptr, or Slice 调用

     Elem() Type

     // 返回结构体类型的第 i 个字段,只能是结构体类型调用

     // 若是 i 超过了总字段数,就会 panic

     Field(i int) StructField

     // 返回嵌套的结构体的字段

     FieldByIndex(index []int) StructField  

     // 经过字段名称获取字段

     FieldByName(name string) (StructField, bool)

     // FieldByNameFunc returns the struct field with a name

     // 返回名称符合 func 函数的字段

     FieldByNameFunc(match func(string) bool) (StructField, bool)

     // 获取函数类型的第 i 个参数的类型

     In(i int) Type

     // 返回 map 的 key 类型,只能由类型 map 调用

     Key() Type

     // 返回 Array 的长度,只能由类型 Array 调用

     Len() int

     // 返回类型字段的数量,只能由类型 Struct 调用

     NumField() int

     // 返回函数类型的输入参数个数

     NumIn() int

     // 返回函数类型的返回值个数

     NumOut() int

     // 返回函数类型的第 i 个值的类型

     Out(i int) Type

     // 返回类型结构体的相同部分

     common() *rtype

     // 返回类型结构体的不一样部分

     uncommon() *uncommonType

}

复制代码

可见 Type 定义了很是多的方法,经过它们能够获取类型的一切信息,你们必定要完整的过一遍上面全部的方法。

注意到 Type 方法集的倒数第二个方法 common返回的 rtype类型,它和上一篇文章讲到的 _type 是一回事,并且源代码里也注释了:两边要保持同步:


// rtype must be kept in sync with ../runtime/type.go:/^type._type.

type rtype struct{

    size       uintptr

   ptrdata    uintptr

    hash       uint32

    tflag      tflag

    align      uint8

   fieldAlign  uint8

    kind       uint8

    alg        *typeAlg

    gcdata     *byte

    str        nameOff

    ptrToThis  typeOff

}
复制代码

全部的类型都会包含 rtype 这个字段,表示各类类型的公共信息;另外,不一样类型包含本身的一些独特的部分。

好比下面的 arrayType 和 chanType 都包含 rytpe,而前者还包含 slice,len 等和数组相关的信息;后者则包含 dir 表示通道方向的信息。

// arrayType represents a fixed array type.

type arrayType struct{

    rtype `reflect:"array"`

    elem *rtype // array element type

    slice *rtype // slice type 

    len   uintptr

}

// chanType represents a channel type.

type chanType struct{

    rtype `reflect:"chan"`

    elem *rtype // channel element type

    dir uintptr // channel direction (ChanDir)

}

复制代码

注意到, Type 接口实现了 String() 函数,知足 fmt.Stringer 接口,所以使用 fmt.Println 打印的时候,输出的是 String() 的结果。另外, fmt.Printf()函数,若是使用 %T 来做为格式参数,输出的是 reflect.TypeOf 的结果,也就是动态类型。

例如:

fmt.Printf("%T", 3) // int
复制代码

讲完了 TypeOf 函数,再来看一下 ValueOf 函数。返回值 reflect.Value 表示 interface{} 里存储的实际变量,它能提供实际变量的各类信息。相关的方法经常是须要结合类型信息和值信息。

例如,若是要提取一个结构体的字段信息,那就须要用到 _type (具体到这里是指 structType) 类型持有的关于结构体的字段信息、偏移信息,以及 *data 所指向的内容 —— 结构体的实际值。

源码以下:

func ValueOf(i interface{}) Value{

   ifi == nil{

     return Value{}

    }

   // ……

    return unpackEface(i)

}

// 分解 eface

func unpackEface(i interface{}) Value{

   e := (*emptyInterface)(unsafe.Pointer(&i))

   t := e.typ

   ift == nil{

      return Value{}

    }

    f := flag(t.Kind())

    if ifaceIndir(t) {

        f |= flagIndir

    }

    return Value{t, e.word, f}

}
复制代码

从源码看,比较简单:将先将 i 转换成 *emptyInterface 类型, 再将它的 typ 字段和 word 字段以及一个标志位字段组装成一个 Value 结构体,而这就是 ValueOf函数的返回值,它包含类型结构体指针、真实数据的地址、标志位。

Value 结构体定义了不少方法,经过这些方法能够直接操做 Value 字段 ptr 所指向的实际数据:

// 设置切片的 len 字段,若是类型不是切片,就会panic

func (v Value) SetLen(n int)

// 设置切片的 cap 字段

func (v Value) SetCap(n int)

// 设置字典的 kv

func (v Value) SetMapIndex(key, val Value)

// 返回切片、字符串、数组的索引 i 处的值

func (v Value) Index(i int) Value

// 根据名称获取结构体的内部字段值

func (v Value) FieldByName(name string) Value

// ……

复制代码

Value 字段还有不少其余的方法。

例如:

// 用来获取 int 类型的值

func (v Value) Int() int64

// 用来获取结构体字段(成员)数量

func (v Value) NumField() int

// 尝试向通道发送数据(不会阻塞)

func (v Value) TrySend(x reflect.Value) bool

// 经过参数列表 in 调用 v 值所表明的函数(或方法

func (v Value) Call(in[]Value) (r []Value) 

// 调用变参长度可变的函数

func (v Value) CallSlice(in[]Value) []Value

复制代码

不一一列举了,反正是很是多。能够去 src/reflect/value.go 去看看源码,搜索 func(vValue) 就能看到。

另外,经过 Type() 方法和 Interface() 方法能够打通 interface、 Type、 Value 三者。Type() 方法也能够返回变量的类型信息,与 reflect.TypeOf() 函数等价。Interface() 方法能够将 Value 还原成原来的 interface。

这里引用老钱《快学Go语言第十五课——反射》的一张图:



总结一下: TypeOf() 函数返回一个接口,这个接口定义了一系列方法,利用这些方法能够获取关于类型的全部信息; ValueOf() 函数返回一个结构体变量,包含类型信息以及实际值。

用一张图来串一下:


上图中, rtye 实现了 Type 接口,是全部类型的公共部分。emptyface 结构体和 eface 实际上是一个东西,而 rtype 其实和 _type 是一个东西,只是一些字段稍微有点差异,好比 emptyface 的 word 字段和 eface 的 data 字段名称不一样,可是数据型是同样的。

▍反射的三大定律

根据 Go 官方关于反射的博客,反射有三大定律:

  1. Reflection goes from interface value to reflection object.

  2. Reflection goes from reflection object to interface value.

  3. To modify a reflection object, the value must be settable.


第一条是最基本的:反射是一种检测存储在 interface 中的类型和值机制。这能够经过 TypeOf 函数和 ValueOf 函数获得。

第二条实际上和第一条是相反的机制,它将 ValueOf 的返回值经过 Interface() 函数反向转变成 interface 变量。

前两条就是说 接口型变量 和 反射类型对象 能够相互转化,反射类型对象实际上就是指的前面说的 reflect.Type 和 reflect.Value。

第三条不太好懂:若是须要操做一个反射变量,那么它必须是可设置的。反射变量可设置的本质是它存储了原变量自己,这样对反射变量的操做,就会反映到原变量自己;反之,若是反射变量不能表明原变量,那么操做了反射变量,不会对原变量产生任何影响,这会给使用者带来疑惑。因此第二种状况在语言层面是不被容许的。

举一个经典例子:

varx float64 = 3.4

v := reflect.ValueOf(x)

v.SetFloat(7.1) // Error: will panic.
复制代码

执行上面的代码会产生 panic,缘由是反射变量 v 不能表明 x 自己,为何?由于调用 reflect.ValueOf(x) 这一行代码的时候,传入的参数在函数内部只是一个拷贝,是值传递,因此 v 表明的只是 x 的一个拷贝,所以对 v 进行操做是被禁止的。

可设置是反射变量 Value 的一个性质,但不是全部的 Value 都是可被设置的。

就像在通常的函数里那样,当咱们想改变传入的变量时,使用指针就能够解决了。

varx float64 = 3.4

p := reflect.ValueOf(&x)

fmt.Println("type of p:", p.Type())

fmt.Println("settability of p:", p.CanSet())

复制代码

输出是这样的:

type of p: *float64

settability of p: false
复制代码

p 还不是表明 x, p.Elem() 才真正表明 x,这样就能够真正操做 x 了:

v := p.Elem()

v.SetFloat(7.1)

fmt.Println(v.Interface()) // 7.1

fmt.Println(x) // 7.1
复制代码

关于第三条,记住一句话:若是想要操做原变量,反射变量 Value 必需要 hold 住原变量的地址才行。

▍反射相关函数的使用

▍代码样例

网络上各类博客文章里使用反射的样例代码很是多,读过这篇文章后,基本没有看不懂的,哈哈!不过,我这里仍是举一个例子,并讲解一番:

package main

import(

   "reflect"

   "fmt"

)

type Childstruct{

    Name string

    Grade int

    Handsome bool

}

type Adult struct{

     ID       string`qson:"Name"`

     Occupation string

     Handsome  bool

}

// 若是输入参数 i 是 Slice,元素是结构体,有一个字段名为 `Handsome`,

// 而且有一个字段的 tag 或者字段名是 `Name` ,

// 若是该 `Name` 字段的值是 `qcrao`,

// 就把结构体中名为 `Handsome` 的字段值设置为 true。

func handsome(i interface{}) {

    // 获取 i 的反射变量 Value

    v := reflect.ValueOf(i)

    // 肯定 v 是一个 Slice

    ifv.Kind() != reflect.Slice{

      return

    }

    // 肯定 v 是的元素为结构体

   if e := v.Type().Elem(); e.Kind() != reflect.Struct{

      return

    }

    // 肯定结构体的字段名含有 "ID" 或者 json tag 标签为 `name`

    // 肯定结构体的字段名 "Handsome"

    st := v.Type().Elem()

    // 寻找字段名为 Name 或者 tag 的值为 Name 的字段

    foundName := false

    fori := 0; i < st.NumField(); i++ {

       f := st.Field(i)

     tag := f.Tag.Get("qson")

     if(tag == "Name"|| f.Name== "Name") && f.Type.Kind() == reflect.String{

        foundName = true

        break

      }

    }

    if!foundName {

       return

    }


    if niceField, foundHandsome := st.FieldByName("Handsome"); foundHandsome == false|| niceField.Type.Kind() != reflect.Bool{

      return

    }

    // 设置名字为 "qcrao" 的对象的 "Handsome" 字段为 true

   fori := 0; i < v.Len(); i++ {

      e := v.Index(i)

      handsome := e.FieldByName("Handsome")

      // 寻找字段名为 Name 或者 tag 的值为 Name 的字段

      varname reflect.Value

      forj := 0; j < st.NumField(); j++ {

         f := st.Field(j)

       tag := f.Tag.Get("qson")

       if tag == "Name"|| f.Name== "Name"{

          name = v.Index(i).Field(j)

       } 

     }

     if name.String() == "qcrao"{

        handsome.SetBool(true)  

     }

   }

}

func main() {

     children := []Child{

         {Name: "Ava", Grade: 3, Handsome: true},

         {Name: "qcrao", Grade: 6, Handsome: false},

    }

    adults := []Adult{

        {ID: "Steve", Occupation: "Clerk", Handsome: true},

        {ID: "qcrao", Occupation: "Go Programmer", Handsome: false},

    }

    fmt.Printf("adults before handsome: %v\n", adults)

    handsome(adults)

    fmt.Printf("adults after handsome: %v\n", adults)

    fmt.Println("-------------")

    fmt.Printf("children before handsome: %v\n", children)

    handsome(children)

    fmt.Printf("children after handsome: %v\n", children)

}

复制代码

代码运行结果:

adults before handsome: [{SteveClerktrue} {qcrao GoProgrammerfalse}]

adults after handsome: [{SteveClerktrue} {qcrao GoProgrammertrue}]

-------------

children before handsome: [{Ava3true} {qcrao 6false}]

children after handsome: [{Ava3true} {qcrao 6true}]
复制代码

代码主要作的事情是:找出传入的参数为 Slice,而且 Slice 的元素为结构体,若是其中有一个字段名是 Name 或者是 标签名称为 Name,而且还有一个字段名是 Handsome的情形。若是找到,而且字段名称为 Name 的实际值是 qcrao 的话,就把另外一个字段 Handsome 的值置为 true。

程序并不关心传入的结构体究竟是什么,只要它的字段名包含 Name 和 Handsome,都是 handsome 函数要工做的对象。

注意一点, Adult 结构体的标签 qson:"Name",中间是没有空格的,不然 Tag.Get("qson") 识别不出来。

▍未导出成员

利用反射机制,对于结构体中未导出成员,能够读取,但不能修改其值。

注意,正常状况下,代码是不能读取结构体未导出成员的,但经过反射能够越过这层限制。另外,经过反射,结构体中能够被修改的成员只有是导出成员,也就是字段名的首字母是大写的。


一个可取地址的 reflect.Value 变量会记录一个结构体成员是不是未导出成员,若是是的话则拒绝修改操做。 CanAddr 不能说明一个变量是否能够被修改。 CanSet 则能够检查对应的 reflect.Value 是否可取地址并可被修改。

package main

import(

    "reflect"

    "fmt"

)

type Child struct{

     Name string

     handsome bool

}

func main() {

    qcrao := Child{Name: "qcrao", handsome: true}

    v := reflect.ValueOf(&qcrao)

    f := v.Elem().FieldByName("Name")

    fmt.Println(f.String())

    f.SetString("stefno")

    fmt.Println(f.String())

    f = v.Elem().FieldByName("handsome")

    // 这一句会致使 panic,由于 handsome 字段未导出

    //f.SetBool(true)

    fmt.Println(f.Bool())

}
复制代码

执行结果:

qcrao

stefno

true
复制代码

上面的例子中,handsome 字段未导出,能够读取,但不能调用相关 set 方法,不然会 panic。反射用起来必定要当心,调用类型不匹配的方法,会致使各类 panic。

▍反射的实际应用

反射的实际应用很是广:IDE 中的代码自动补全功能、对象序列化(json 函数库)、fmt 相关函数的实现、ORM(全称是:Object Relational Mapping,对象关系映射)……

这里举 2 个例子:json 序列化和 DeepEqual 函数。

▍json 序列化

开发过 web 服务的同窗,必定用过 json 数据格式。 json 是一种独立于语言的数据格式。最先用于浏览器和服务器之间的实时无状态的数据交换,并由此发展起来。

Go 语言中,主要提供 2 个函数用于序列化和反序列化:

func Marshal(v interface{}) ([]byte, error)

func Unmarshal(data []byte, v interface{}) error

复制代码

两个函数的参数都包含 interface,具体实现的时候,都会用到反射相关的特性。

对于序列化和反序列化函数,均须要知道参数的全部字段,包括字段类型和值,再调用相关的 get 函数或者 set 函数进行实际的操做。

▍DeepEqual 的做用及原理

在测试函数中,常常会须要这样的函数:判断两个变量的实际内容彻底一致。

例如:如何判断两个 slice 全部的元素彻底相同;如何判断两个 map 的 key 和 value 彻底相同等等。

上述问题,能够经过 DeepEqual 函数实现。

func DeepEqual(x, y interface{}) bool

复制代码

DeepEqual 函数的参数是两个 interface,实际上也就是能够输入任意类型,输出 true 或者 flase 表示输入的两个变量是不是“深度”相等。

先明白一点,若是是不一样的类型,即便是底层类型相同,相应的值也相同,那么二者也不是“深度”相等。

type MyIntint

type YourIntint

func main() {

    m := MyInt(1)

    y := YourInt(1)

    fmt.Println(reflect.DeepEqual(m, y)) // false

}

复制代码

上面的代码中,m, y 底层都是 int,并且值都是 1,可是二者静态类型不一样,前者是 MyInt,后者是 YourInt,所以二者不是“深度”相等。

在源码里,有对 DeepEqual 函数的很是清楚地注释,列举了不一样类型,DeepEqual 的比较情形,这里作一个总结:

类型

深度相等情形


通常状况下,DeepEqual 的实现只须要递归地调用 == 就能够比较两个变量是不是真的“深度”相等。

可是,有一些异常状况:好比 func 类型是不可比较的类型,只有在两个 func 类型都是 nil 的状况下,才是“深度”相等;float 类型,因为精度的缘由,也是不能使用 == 比较的;包含 func 类型或者 float 类型的 struct, interface, array 等。

对于指针而言,当两个值相等的指针就是“深度”相等,由于二者指向的内容是相等的,即便二者指向的是 func 类型或者 float 类型,这种状况下不关心指针所指向的内容。

一样,对于指向相同 slice, map 的两个变量也是“深度”相等的,不关心 slice, map 具体的内容。

对于“有环”的类型,好比循环链表,比较二者是否“深度”相等的过程当中,须要对已比较的内容做一个标记,一旦发现两个指针以前比较过,当即中止比较,并断定两者是深度相等的。这样作的缘由是,及时中止比较,避免陷入无限循环。

来看源码:

func DeepEqual(x, y interface{}) bool{

    ifx == nil|| y == nil{

       returnx == y

   }

   v1 := ValueOf(x)

   v2 := ValueOf(y)

   if v1.Type() != v2.Type() {

       return false

   }

   return deepValueEqual(v1, v2, make(map[visit]bool), 0)

}
复制代码

首先查看二者是否有一个是 nil 的状况,这种状况下,只有二者都是 nil,函数才会返回 true。

接着,使用反射,获取x,y 的反射对象,而且当即比较二者的类型,根据前面的内容,这里其实是动态类型,若是类型不一样,直接返回 false。

最后,最核心的内容在子函数 deepValueEqual 中。

代码比较长,思路却比较简单清晰:核心是一个 switch 语句,识别输入参数的不一样类型,分别递归调用 deepValueEqual 函数,一直递归到最基本的数据类型,比较 int,string 等能够直接得出 true 或者 false,再一层层地返回,最终获得“深度”相等的比较结果。

实际上,各类类型的比较套路比较类似,这里就直接节选一个稍微复杂一点的 map 类型的比较:

// deepValueEqual 函数

// ……

case Map:
   if v1.IsNil() != v2.IsNil() {

       return false

  }

  if v1.Len() != v2.Len() {

     return false

   }

   if v1.Pointer() == v2.Pointer() {

      return true

    }

    for_, k := range v1.MapKeys() {

       val1 := v1.MapIndex(k)

       val2 := v2.MapIndex(k)

       if!val1.IsValid() || !val2.IsValid() || !deepValueEqual(v1.MapIndex(k), v2.MapIndex(k), visited, depth+1) {

       return false

      }

    }

    return true

// ……  
复制代码

和前文总结的表格里,比较 map 是否相等的思路比较一致,也不须要多说什么。说明一点, visited 是一个 map,记录递归过程当中,比较过的“对”:

type visit struct{

    a1 unsafe.Pointer

    a2 unsafe.Pointer

    typ Type

}

map[visit]bool
复制代码

比较过程当中,一旦发现比较的“对”,已经在 map 里出现过的话,直接断定“深度”比较结果的是 true。

▍总结

Go 做为一门静态语言,相比 Python 等动态语言,在编写过程当中灵活性会受到必定的限制。可是经过接口加反射实现了相似于动态语言的能力:能够在程序运行时动态地捕获甚至改变类型的信息和值。

Go 语言的反射实现的基础是类型,或者说是 interface,当咱们使用反射特性时,实际上用到的就是存储在 interface 变量中的和类型相关的信息,也就是常说的 <type,value> 对。

只有 interface 才有反射的说法。

反射在 reflect 包中实现,涉及到两个相关函数:

func TypeOf( i interface{} ) Type

func ValueOf( i interface{} ) Value
复制代码

Type 是一个接口,定义了不少相关方法,用于获取类型信息。Value 则持有类型的具体值。Type、Value、Interface 三者间经过函数 TypeOf,ValueOf,Interface 进行相互转换。

最后温习一下反射三大定律:

1. Reflection goes from interface value to reflection object.

2. Reflection goes from reflection object to interface value.

3. To modify a reflection object, the value must be settable.

翻译一下:

1. 反射将接口变量转换成反射对象 Type 和 Value;

2. 反射能够经过反射对象 Value 还原成原先的接口变量;

3. 反射能够用来修改一个变量的值,前提是这个值能够被修改。

▍参考资料

点击此处了解

▍END


相关文章
相关标签/搜索