2021-08-29 02:30:20

go反射

go标准库-reflect

[TOC]

Golang 接口与反射知识要点

这篇文章以 Go 官方经典博客 The Laws of Reflection 为基础,详细介绍文中涉及的知识点,并有所扩展。

interface

首先,我们谈谈接口类型的内存布局(memory layout),其他基础类型、Struct、Slice、Map、指针类型的内存布局会在以后单独分析。接口变量的值包含两部分内容:赋值给接口类型变量的实际值(concrete value),实际值的类型信息(type descriptor)。两部分在一起构成接口的值(interface value)。

接口变量的这两部分内容由两个字来存储(假设是 32 位系统,那么一个字就是 32 位),第一个字指向 itable (interface table)。itable 表示 interface 和实际类型的转换信息。itable 开头是一个存储了变量实际类型的描述信息,接着是一个由函数指针组成的列表。注意 itable 中的函数和接口类型相对应,而不是和动态类型。例如下面例子,itable 只关联了 Stringer 中定义的 String 方法,而 Binary 中定义的 Get 方法则不在其中。对于每个 interface 和实际类型,只要在代码中存在引用关系, go 就会在运行时为这一对具体的 <Interface, Type> 生成 itable 信息。

第二个字称为 data,指向实际的数据。例子中,赋值语句 var s Stringer = b 实际上对b做了拷贝,而不是对b进行引用。存放在接口变量中的数据大小可能任意,但接口只提供了一个字来专门存储真实数据,所以赋值语句在堆上分配了一块内存,并将该字设置为对这块内存的引用。

type Stringer interface { String() string } type Binary uint64 func (i Binary) String() string { return strconv.Uitob64(i.Get(), 2) } func (i Binary) Get() uint64 { return uint64(i) } b := Binary(200) var s Stringer = b

83c223f726b4b235248e3e22d341d32a.png

Go 是静态类型语言(statically typed)。一个接口类型的不同变量总是有同样静态类型,尽管在运行时,接口变量的保存的实际值会改变。下面例子中,无论 r 被赋予的什么实际值,r 的静态类型总是 io.Reader。

var r io.Reader r = os.Stdin r = bufio.NewReader(r) r = new(bytes.Buffer) // and so on

interface 的底层结构

interface有2个标示:

  1. eface 表示空的 interface{},它用两个机器字长表示,第一个字 _type 是指向实际类型描述的指针,第二个字 data 代表数据指针。
  2. iface 表示至少带有一个函数的 interface, 它也用两个机器字长表示,第一个字 tab 指向一个 itab 结构,第二个字 data 代表数据指针。

e9ad9d6d418c4c033ff7473632219dcb.jpeg

  • size 为该类型所占用的字节数量。
  • kind 表示类型的种类,如 bool、int、float、string、struct、interface 等。
  • str 表示类型的名字信息,它是一个 nameOff(int32) 类型,通过这个 nameOff,可以找到类型的名字字符串
  • 灰色的 extras 对于基础类型(如 bool,int, float 等)是 size 为 0 的,它为复杂的类型提供了一些额外信息。例如为 struct 类型提供 structtype,为 slice 类型提供 slicetype 等信息。
  • 灰色的 ucom 对于基础类型也是 size 为 0 的,但是对于 type Binary int 这种定义或者是其它复杂类型来说,ucom 用来存储类型的函数列表等信息。
  • 注意 extras 和 ucom 的圆头箭头,它表示 extras 和 ucom 不是指针,它们的内容位于 _type 的内存空间中。
type iface struct { tab *itab data unsafe.Pointer } type itab struct { inter *interfacetype //指向对应的 interface 的类型信息。 _type *_type //type 和 eface 中的一样,指向的是实际类型的描述信息 _type link *itab hash uint32 bad bool inhash bool unused [2]byte fun [1]uintptr //fun 为函数列表,表示对于该特定的实际类型而言,interface 中所有函数的地址。 }

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

04f68faf289c835a86853af79da3739b.png

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

type eface struct { _type *_type data unsafe.Pointer }

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

1d92ac3dbe16c75e5f58e49b02c26978.png

类型断言

类型断言是一个使用在接口变量上的操作。
Go 语言中最常见的就是 ReaderWriter 接口:

type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) }

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

var r io.Reader tty, err := os.OpenFile("/Users/qcrao/Desktop/test", os.O_RDWR, 0) if err != nil { return nil, err } r = tty

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

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

f2cc593a14060cb588e561fe39ea2f37.png

注意看上图,此时虽然 fun 所指向的函数只有一个 Read 函数,其实 *os.File 还包含 Write 函数,也就是说 *os.File 其实还实现了 io.Writer 接口。因此下面的断言语句可以执行:

var w io.Writer w = r.(io.Writer)

该赋值语句后边是一个类型断言。它断言的是 r 变量携带的元素,同时是 io.Writer 接口的实现,所以我们才能把 r 赋值给 w。赋值后的 w 可以访问 Write 方法,但无法访问 Read 方法了。w 的内存形式如下图:
7ede4b63e93c54b8aaf842b6e4cd8d1b.png

最后,再来一个赋值:

var empty interface{} empty = w

由于 empty 是一个空接口,因此所有的类型都实现了它,w 可以直接赋给它,不需要执行断言操作。
552a0e54430902aa397f5b91abfd65c2.png

从上面的三张图可以看到,interface 包含三部分信息:_type 是类型信息,*data 指向实际类型的实际值,itab 包含实际类型的信息,包括大小、包路径,还包含绑定在类型上的各种方法(图上没有画出方法),补充一下关于 os.File 结构体的图:
7e93b8d6ecd2ba020b4b049825cad4e9.png

鸭子类型

鸭子类型(duck typing)是动态类型和某些静态语言用到的一种对象推断风格。一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由"当前方法和属性的集合"决定。这个概念也可以表述为:

当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。

鸭子类型像多态一样工作,但是没有继承。在鸭子类型中,关注点在于对象的行为,能作什么;而不是关注对象所属的类型。在常规类型中,我们能否在一个特定场景中使用某个对象取决于这个对象的类型,而在鸭子类型中,则取决于这个对象是否具有某种属性或者方法 —— 即只要具备特定的属性或方法,能通过鸭子类型测试,就可以使用。鸭子类型的缺点是没有任何静态检查,如类型检查、属性检查、方法签名检查等。

Go 语言虽然是静态语言,但在接口类型中使用了鸭子类型。不同于其他鸭子类型语言的是,它实现了在编译时进行静态检查,比如变量是否实现接口方法、调用接口方法时参数个数是否相符,同时也不失鸭子类型带来的灵活和自由。

interface nil陷阱

当一个指针赋值给 interface 类型时,无论此指针是否为 nil,赋值过的 interface 都不为 nil(因为带上了类型信息)。

reflect

反射机制

什么是反射机制?

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

简单来说,反射只是一种机制,在程序运行时获得对象类型信息和内存结构。 通常高级语言借助反射机制来解决,编译时无法知道变量具体类型,而只有等到运行时才能检查值和类型的问题。不同语言的反射模型不尽相同,有些语言还不支持反射。对于低级语言,比如汇编语言,由于自身可以直接和内存打交道,所以无需反射机制。

使用反射的场景?

Go 语言中使用反射的场景:

有时候需要根据某些条件决定调用哪个函数,比如根据用户的输入来决定,但事先无法不知道接受到的参数是什么类型,全部以 interface{} 类型接受。这时就需要对函数的参数进行反射,在运行期间动态地执行函数。感兴趣的读者可以参考 fmt.Sprint(a …interface{}) 方法的源码。

不使用反射的理由:

  1. 与反射相关的代码,经常是难以阅读的。在软件工程中,代码可读性也是一个非常重要的指标。
  2. Go 语言作为一门静态语言,编码过程中,编译器能提前发现一些类型错误,但是对于反射代码是无能为力的。所以包含反射相关的代码,很可能会运行很久,才会出错,这时候经常是直接 panic,可能会造成严重的后果。
  3. 反射对性能影响还是比较大的,比正常代码运行速度慢一到两个数量级。所以,对于一个项目中处于运行效率关键位置的代码,尽量避免使用反射特性。

reflect包

TypeOf()

reflect 包封装了很多简单的方法(reflect.TypeOfreflect.ValueOf)来动态获得类型信息和实际值(reflect.Typereflect.Value)。

看下源码:

func TypeOf(i interface{}) Type { eface := *(*emptyInterface)(unsafe.Pointer(&i)) return toType(eface.typ) }

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

type emptyInterface struct { typ *rtype word unsafe.Pointer }

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

func toType(t *rtype) Type { if t == nil { return nil } return t }

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

type Type interface { // 所有的类型都可以调用下面这些函数 // 此类型的变量对齐后所占用的字节数 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 // 返回 mapkey 类型,只能由类型 map 调用 Key() Type // 返回 Array 的长度,只能由类型 Array 调用 Len() int // 返回类型字段的数量,只能由类型 Struct 调用 NumField() int // 返回函数类型的输入参数个数 NumIn() int // 返回函数类型的返回值个数 NumOut() int // 返回函数类型的第 i 个值的类型 Out(i int) Type // 返回类型结构体的相同部分 common() *rtype // 返回类型结构体的不同部分 uncommon() *uncommonType }

注意到 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 这个字段,表示各种类型的公共信息;另外,不同类型包含自己的一些独特的部分。

比如下面的 arrayTypechanType 都包含 rytpe,而前者还包含 slicelen 等和数组相关的信息;后者则包含 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

ValueOf()

返回值 reflect.Value 表示 interface{} 里存储的实际变量,它能提供实际变量的各种信息。相关的方法常常是需要结合类型信息和值信息。例如,如果要提取一个结构体的字段信息,那就需要用到 _type (具体到这里是指 structType) 类型持有的关于结构体的字段信息、偏移信息,以及 *data 所指向的内容 —— 结构体的实际值。

func ValueOf(i interface{}) Value { if i == nil { return Value{} } // …… return unpackEface(i) } // 分解 eface func unpackEface(i interface{}) Value { e := (*emptyInterface)(unsafe.Pointer(&i)) t := e.typ if t == 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
var x float64 = 3.4 fmt.Println("type:", reflect.TypeOf(x)) // 打印 type: float64 var r io.Reader = strings.NewReader("Hello") fmt.Println("type:", reflect.TypeOf(r)) // 打印 type: *strings.Reader

reflect.TypeOf 方法的函数签名是 func TypeOf(i interface{}) Type 。它接受任意类型的变量。当我们调用 reflect.TypeOf(x) 时,x 首先存储在一个空接口类型中,作为传参。reflect.TypeOf 解析空接口,恢复 x 的类型信息。而调用 reflect.ValueOf 则可以恢复 x 实际值。

var x float64 = 3.4 fmt.Println("value:", reflect.ValueOf(x).String()) // 打印 value: &lt;float64 Value&gt;

Type、Value、interface的相互转换

d5023a21cfb72af0e715834e5db7a33a.png

Type()、Kind()

reflect.Type 和 reflect.Value 都提供了很多方法支持来操作他们。

  1. reflect.Value 的 Type() 方法返回实际类型信息;
  2. reflect.Type 和 reflect.Value 都有 Kind() 方法,来获得实际值的底层类型,结果对应的是 reflect 包中定义的常量;
  3. reflect.Value 的那些以类型名为方法名的方法,比如 Int()、Float(),能获得实际值。
var x float64 = 3.4 v := reflect.ValueOf(x) fmt.Println("type:", v.Type()) fmt.Println("kind is float64:", v.Kind() == reflect.Float64) fmt.Println("value:", v.Float())

打印结果:

shell script type: float64 kind is float64: true value: 3.4

有一点需要注意的是,Kind() 方法返回的是反射对象的底层类型,而不是静态类型。 比如,如果反射对象接受一个用户定义的整数型变量:

func main() { type MyInt int var x MyInt = 7 v := reflect.ValueOf(x) fmt.Println("type:", v.Type()) fmt.Println("kind is int:", v.Kind() == reflect.Int) fmt.Println("value:", v.Int())}

打印结果:

shell script type: main.MyInt kind is int: true value: 7

v 调用 Kind() 仍是 reflect.Int,即使 x 的静态类型是 MyInt 而不是 int。总而言之,Kind() 方法无法区分来自 MyInt 的整数型和 int 型,但 Type() 方法可以。

Interface()

Interface() 方法能从 reflect.Value 变量中恢复接口值,是 ValueOf() 的逆向。注意的是,Interface() 方法返回总是静态类型 interface{}。

反射对象的可设置性

SetXXX(), CanSet()

var x float64 = 3.4 v := reflect.ValueOf(x) v.SetFloat(7.1) // will panic: reflect.Value.SetFloat using unaddressable value

运行上面的例子,我们可以发现 v 不可修改(settable)。可设置性(Settability)是 reflect.Value 的一个特性,但不是所有的 Value 都有。

var x float64 = 3.4 v := reflect.ValueOf(x) fmt.Println("settability of v:", v.CanSet()) // settability of v: false

Elem()

由于是 x 的一个拷贝传入 reflect.ValueOf,所以 reflect.ValueOf 创建的接口值也是 x 的一个拷贝,不是原 x 本身。因此修改反射对象,无法修改 x,反射对象不具有可设置性。显然,要使反射对象具有可设置性。传入 reflect.ValueOf 的参数应该是 x 的地址,即 &x。

var x float64 = 3.4 p := reflect.ValueOf(&x) // Note: take the address of x. fmt.Println("type of p:", p.Type()) // type of p: *float64 fmt.Println("settability of p:", p.CanSet()) // settability of p: false

反射对象 p 仍是不可设置的,因为我们不是要设置 p,而是 p 所指向的内容。使用 Elem 方法获取。

// Elem returns the value that the interface v contains // or that the pointer v points to. // It panics if v's Kind is not Interface or Ptr. // It returns the zero Value if v is nil. func (v Value) Elem() Value
v := p.Elem() fmt.Println("settability of v:", v.CanSet()) // settability of v: true v.SetFloat(7.1) fmt.Println(v.Interface()) // 7.1 fmt.Println(x) // 7.1

Struct 的反射

NumField(), Type.Field(i int)

我们用 struct 的地址来创建反射对象,这样后续我们可以修改这个 struct:

type T struct { A int B string } t := T{23, "skidoo"} s := reflect.ValueOf(&t).Elem() typeOfT := s.Type() for i := 0; i < s.NumField(); i++ { f := s.Field(i) fmt.Printf("%d: %s %s = %v\n", i, typeOfT.Field(i).Name, f.Type(), f.Interface()) }

Type.Field(i int) 方法返回字段信息,一个 StructField 类型的对象,包含字段名等。打印结果:

0: A int = 23
1: B string = skidoo

Value.Field(i int)

T 的字段必须是首字母大写的才可以设置,因为只有暴露的 struct 字段,才具有可设置性。

s.Field(0).SetInt(77) s.Field(1).SetString("Sunset Strip") fmt.Println("t is now", t) // t is now {77 Sunset Strip}

Value.Field(i int) 返回 struct s 的字段实际值,所以可以用来设置操作。 注意 Type.Field(i int) 和 Value.Field(i int) 的用途区别:前者总是负责和实际类型信息获取相关的操作,后者是与实际值相关的操作。

指针的设置

由于指针可能是nil, 设置有点不一样。

switch s.Field(i).Type.Kind() { case reflect.Ptr: target := s.Field(i).Type.Elem() switch target.Kind() { case reflect.Int: if !e.Field(i).IsNil() { e.Field(i).Elem().SetInt(int64(v.(float64))) } else { var v = int64(v.(float64)) vv := reflect.New(e.Field(i).Type().Elem()) vv.Elem().SetInt(v) e.Field(i).Set(vv) } ...

先判断IsNil(), 不是nil可以直接设置。 SetInt
但是如果是nil,就需要构造一个带指针的Value,然后直接设置Set(v)

反射的三大定律

  1. Reflection goes from interface value to reflection object.
    第一条是最基本的:反射是一种检测存储在 interface 中的类型和值机制。这可以通过 TypeOf 函数和 ValueOf 函数得到。
  1. Reflection goes from reflection object to interface value.
    第二条实际上和第一条是相反的机制,它将 ValueOf 的返回值通过 Interface() 函数反向转变成 interface 变量。

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

  1. To modify a reflection object, the value must be settable.
    第三条不太好懂:如果需要操作一个反射变量,那么它必须是可设置的。反射变量可设置的本质是它存储了原变量本身,这样对反射变量的操作,就会反映到原变量本身;反之,如果反射变量不能代表原变量,那么操作了反射变量,不会对原变量产生任何影响,这会给使用者带来疑惑。所以第二种情况在语言层面是不被允许的。

参考文档

The Laws of Reflection

Go Data Structures: Interfaces

Go 语言的数据结构:Interfaces

浅析 Golang Interface 实现原理

深度解密Go语言之反射

本文链接:http://blog.go2live.cn/post/go-reflect.html

-- EOF --