深入Go语言 - 13

反射

目录 [−]

  1. Type
  2. Value
  3. 转换

本章重点介绍Go语言中的反射。
reflect可以实现运行时的反射,允许程序操纵对象的值和类型。
典型地,你可以获取 interface{}的动态类型以及的它的值和方法。

Go是静态类型的语言,每一个对象在声明和初始化的时候都已经有一个确定值,即使是声明为接口类型的变量,它的静态类型也已经确定,即使任何包含这个接口方法集的类型的对象都可以赋值给它。

我们可以在运行时获取对象的动态类型和值。

类型Type和值Value是我们使用发射库的主要用的两个概念。

Type

Type是一个interface,代表Go中的一个类型,可以把它看成某个类型的元数据(描述类型的类型),这个类型既可以是Go语言或者库中定义的type类型,也可以你自己定义的type类型。

下面我们介绍它的主要方法以及一些辅助方法。

值得注意的是,并不是所有的方法都对某种类型有效,比如有些方法只对函数类型有意义,有的只对Struct有意义。如果对某个类型调用了错误的方法,则会发生运行时的panic,比如针对struct Type,调用IsVariadic方法。所以最好先判断一下Type的Kind。

首先我们先为下面的例子定义一个简单的struct用来测试, Bird有三个字段Field和三个方法,其中两个字段和两个方法是导出的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Bird struct {
Name string
Color string
age int
}
func (b Bird) Sing() string {
return "sing"
}
func (b *Bird) Fly() string {
return "fly"
}
func (b *Bird) food() {
}

TypeOf返回一个接口对象的动态类型,也就是类型reflect.Type的一个值:

1
2
3
var bird = Bird{Name: "parrot", Color: "blue"}
var t reflect.Type
t = reflect.TypeOf(bird)
  • NamePkgPathString
    返回动态类型的名称,包名以及字符串表示。
1
2
3
fmt.Println(t.Name()) //Bird
fmt.Println(t.String()) //main.Bird
fmt.Println(t.PkgPath()) //main

Name返回类型名,如Bird,对于未定义名称的类型,返回空字符串。

PkgPath返回包名,代表这个包的唯一标识符,所以可能是单一的包名,或者encoding/base64。对于Go内置的类型string,error等,或者未定义名称的类型struct{}等,则返回空字符串。

String方法返回这个类型的字符串表示,包名可能用简写表示。

  • Kind
    返回类型的分类。 你自己定义的类型必定属于下面的某一类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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
)

例如:

1
fmt.Println(t.Kind()) //struct
  • SizeBitsAlignFieldAlign
1
2
3
4
5
fmt.Println(t.Size()) //40
//fmt.Println(t.Bits()) //panic
fmt.Println(t.Align()) //8
fmt.Println(t.FieldAlign()) //8
}

Size返回存储这个类型的一个值所需要的字节数,它返回要保存储这个值需要的内存,而不会计算它引用的内存的大小。 比如字符串是以StringHeader类型来存储的,它包含一个指针指向内存字符存储的数据,它的size只计算存储这个结构所需的内存,而不会计算指针指向的数据占用的字节数的,数据会对齐的。

Bits返回类型的size,以bit计算,但是如果类型不是Int、Uint、Float、Complex之一则panic。

Algin是这个类型的一个变量在内存中的对齐后的所用的字节数。FieldAlign指这种类型的变量如果是struct中的字段,那么它对齐后所用的字节数。

对于gc编译器来讲,AlignFieldAlign是一样的,但是对于gccgo来讲,它们可能不同。

还有下面会讲到的reflect.StructField.Offset,它是某个字段在struct中的偏移量,一起考虑了前面字段的对齐。

为什么要对齐?
第一个原因——很多CPU只从对齐的地址开始加载数据,而有的CPU这样做,只是更快一点。
第二个原因——外部总线从内存一次获取的数据往往不是1byte,而是4bytes或许8bytes,或者更多~~
引子知乎

注意Size是值占用的字节数,而Align是变量占的字节数。

比如在我当前的开发环境下(64-bit windows 10), 字符串的Size为16个字节,而字符串的Align为8, int类型的SizeAlign都是8, int32的SizeAlign都是4。

  • ImplementsAssignableToConvertibleToComparable

Implements返回类型是否实现了某个接口,如果参数的类型不是interface类型,则会panic。

1
2
3
4
5
6
var r *io.Reader
t1 := reflect.TypeOf(r).Elem()
t2 := reflect.TypeOf(&os.Stdout).Elem()
fmt.Println(t.Implements(t1)) //false
fmt.Println(t2.Implements(t1)) //true

AssignableTo一个类型的值是否可以赋值给参数指定的类型,下面的例子中Bird类型的指针对象可以赋值给IBird接口:

1
2
3
4
5
6
7
type IBird interface {
Sing() string
Fly() string
}
var i IBird = &bird
fmt.Println(t.AssignableTo(reflect.TypeOf(i).Elem()))

ConvertibleTo 一个类型的值是否可以转换成另一个类型的值:

1
2
3
4
5
type Bird2 Bird
var bird2 Bird2
t3 := reflect.TypeOf(bird2)
fmt.Println(t3.ConvertibleTo(t)) //true
bird = Bird(bird2) //可以转换

Comparable返回类型是否可以比较:

1
fmt.Println(t.Comparable()) //true

类型如果可以比较,就可以使用==!=运算符。可以参看 比较运算符

  • NumFieldFieldFieldByIndexFieldByNameFieldByNameFunc
    这一组方法用来获取Struct的Field的信息,这个信息通过StructField类型来描述。如果类型不是Struct,调用相应的方法导致运行时panic。

NumField返回Struct的字段的数量。

1
fmt.Println(t4.NumField()) //1

Field返回struct的第i个字段的信息,包括未导出对象的信息

1
2
3
fmt.Println(t.Field(0)) //{Name string 0 [0] false}
fmt.Println(t.Field(1)) //{Color string 16 [1] false}
fmt.Println(t.Field(2)) //{age github.com/smallnest/dive-into-go/ch12/model int 32 [2] false}

FieldByIndex对于嵌套的Struct,可以递归地得到某一层的字段的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type S1 struct {
Name string
Age int
}
type S2 struct {
S1
}
type S3 struct {
S2
}
var s S3
t4 := reflect.TypeOf(s)
fmt.Println(t4.FieldByIndex([]int{0, 0, 1})) //{Age int 16 [1] false}

这个例子S3嵌套S2,S2嵌套S1,我们通过0,0,1就可以得到S1的字段Age的信息。

FieldByName根据名称得到字段的信息:

1
fmt.Println(t4.FieldByName("Name")) //{Name string 0 [0 0 0] false} true

FieldByNameFunc根据一个函数筛选字段,返回第一个符合条件的字段:

1
2
3
4
5
6
7
fmt.Println(t4.FieldByNameFunc(func(n string) bool {
if n == "Age" {
return true
}
return false
}))
  • NumMethodMethodMethodByName
    这是一组操作类型的方法的一组方法。

对于非接口的类型T或者*T,它的方法类型和函数字段描述了方法的第一个参数就是receiver。

对于接口类型,它的方法类型的Func字段总是为nil。

NumMethod返回这个类型的方法集中方法的数量, Method返回第i个方法的信息:

1
2
3
4
5
6
7
fmt.Println(t.NumMethod()) //1
fmt.Println(t.Method(0)) //{Sing func(model.Bird) string <func(model.Bird) string Value> 0}
t5 := reflect.TypeOf(&bird)
fmt.Println(t5.NumMethod()) //3
fmt.Println(t5.Method(0)) //{Fly func(*model.Bird) string <func(*model.Bird) string Value> 0}
fmt.Println(t5.Method(1)) //{Sing func(*model.Bird) string <func(*model.Bird) string Value> 1}
fmt.Println(t5.Method(2)) //{food github.com/smallnest/dive-into-go/ch12/model func(*model.Bird) <func(*model.Bird) Value> 2}

MethodByName则是根据名称查找方法。

  • NumInInNumOutOutIsVariadic
    对于函数类型,我们关注的是它的输入参数信息Int和输出参数信息Out
    IsVariadic返回函数是否是变参的。
1
2
3
4
5
6
7
f := http.ListenAndServe
ft := reflect.TypeOf(f)
fmt.Println(ft.NumIn()) //2
fmt.Println(ft.In(0)) //string
fmt.Println(ft.NumOut()) //1
fmt.Println(ft.Out(0)) //error
fmt.Println(ft.IsVariadic()) //false
  • Key
    Key方法返回map类型的key的类型,如果不是map类型,则调用此方法会panic。
1
2
3
var m map[string]int
mt := reflect.TypeOf(m)
fmt.Println(mt.Key())
  • Elem
    Elem方法返回类型 Array, Chan, Map, Ptr, Slice的元素的类型。

对于Map类型,它返回的值的类型。

对于指针类型Ptr,它返回指针指向的元素的类型。

对于Chan,它返回传递的元素的类型。

数组和Slice返回的是它包含的元素的类型。

  • Len
    Len返回数组的长度,因为数组的类型中包含长度的定义。如果不是数组则会panic。

  • ChanDir
    返回channel的方向, chan、chan<-、<-chan

Value

Value描述对象的值信息,同样,并不是所有的方法对任何的类型都有意义,特定的方法只适用于特定的类型。

零值代表没有值,它的IsValid总是返回false,它的Kind总是返回Invalid,它的String总是返回""。

A Value can be used concurrently by multiple goroutines provided that the underlying Go value can be used concurrently for the equivalent direct operations.

Using == on two Values does not compare the underlying values they represent, but rather the contents of the Value structs. To compare two Values, compare the results of the Interface method.

本文中不准备详细阐述Value对象的方法。不是这些方法不重要,而是它的方法太多了。

它针对每一中类型的操作都提供了相应的方法,比如slice类型,有Append、AppendSlice等方法、Map有MapIndex、MapKeys等方法、Struct有Field、FieldByIndex等方法。 注意Value的Field方法返回的是字段的值类型Value,而不是字段的类型描述Type。还有针对数值型、Bool型、Channel类型的方法等。

Indirect返回指针指向的对象,如果不是指针,则返回参数本身。

Addr返回一个可寻址的对象的指针。 只有slice的元素、可寻址的数组的元素、可寻址的struct的字段、指针的可寻址的结果才可以调用Addr方法。可以通过CabAddr检查。可以查看Go规范中的描述:Address operators

Zero返回类型的零值。

一组SetXxx方法用来设置值的值(好绕口)。

下面是一组辅助函数,生成Value对象:
MakeChanMakeFuncMakeMapMakeSliceNewNewAt生成某个类型的值Value。
ValueOf从接口对象返回Value。

转换

1、 从接口对象到反射对象

1
2
3
var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))
fmt.Println("value:", reflect.ValueOf(x))

2、从反射对象到接口对象

1
func (v Value) Interface() interface{}

如果想转为特定的类型的对象,可以用type assertion:

1
y := v.Interface().(float64)

3、修改反射对象
对象的值必须是可设置的,可以用CanSet方法判断。

1
2
3
4
5
6
var x float64 = 3.4
p := reflect.ValueOf(&x)
v := p.Elem()
v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)

也有一些第三方的简化go reflect的库,如go-reflector

反射经常用在序列化和反序列的实现中,如官方的jsonxml库。

但是请记住一点,发射的性能并不高,所以很多序列化库采用代码模版的方式生成Model对象,而不是反射的方式序列化和反序列化对象。

参考