一、reflect-反射-浅析-重要
反射是众多编程语言中的一个非常实用的功能,它是一种能够自描述、自控制的应用,Go语言也对反射提供了友好的支持。
Go语言中使用反射可以在编译时不知道类型的情况下更新变量,在运行时查看值、调用方法以及直接对他们的布局进行操作。
由于反射是建立在类型系统(type system)上的,所以我们先来复习一下Go语言中的类型。
Go语言中的类型
Go语言是一门静态类型的语言,每个变量都有一个静态类型,类型在编译的时候确定下来。
type MyInt int var i int var j MyInt
变量 i 的类型是 int,变量 j 的类型是 MyInt,虽然它们有着相同的基本类型,但静态类型却不一样,在没有类型转换的情况下,它们之间无法互相赋值。
接口是一个重要的类型,它意味着一个确定的方法集合,一个接口变量可以存储任何实现了接口的方法的具体值(除了接口本身),例如 io.Reader 和 io.Writer:
// Reader是包装基本Read方法的接口。 type Reader interface { Read(p []byte) (n int, err error) } // Writer是包装基本Write方法的接口。 type Writer interface { Write(p []byte) (n int, err error) }
如果一个类型声明实现了 Reader(或 Writer)方法,那么它便实现了 io.Reader(或 io.Writer),这意味着一个 io.Reader 的变量可以持有任何一个实现了 Read 方法的的类型的值。
// 例一: var r io.Reader r = os.Stdin fmt.Printf("%T\n",r) // *os.File r = bufio.NewReader(r) fmt.Printf("%T\n",r) // *bufio.Reader r = new(bytes.Buffer) fmt.Printf("%T\n",r) // *bytes.Buffer // 三者都是实现了io.Reader接口的结构体
必须要弄清楚的一点是,不管变量 r 中的具体值是什么,r 的类型永远是 io.Reader,由于Go语言是静态类型的,r 的静态类型就是 io.Reader。
// 例二: type person interface { speak() string } type student struct { name string } func (h student) speak() string { return h.name } func main() { var per person fmt.Printf("%T\n",per) // <nil> per = student{name: "胡宇洋"} fmt.Printf("%T\n",per) // main.student }
可以看到student 结构体实现了person接口,per的具体值是一个student结构体,但由于GO语言是静态类型的,per的静态类型就是person。
如果不知道咋回事(我就不知道)就先略过,这个需要看interface深度解析,里面涉及汇编等东西,在以后有时间可以看一下。
在接口类型中有一个极为重要的例子——空接口:
interface{}
它表示了一个空的方法集,一切值都可以满足它,因为它们都有零值或方法。
有人说Go语言的接口是动态类型,这是错误的,它们都是静态类型,虽然在运行时中,接口变量存储的值也许会变,但接口变量的类型是不会变的。
我们必须精确地了解这些,因为反射与接口是密切相关的。
关于接口我们就介绍到这里,下面我们看看Go语言的反射三定律。
反射第一定律:反射可以将“接口类型变量”转换为“反射类型对象”
即:TypeOf(i interface{})和ValueOf(i interface{})方法返回反射类型:reflect.Type类型和reflect.Value类型的变量
注:这里反射类型指 reflect.Type和 reflect.Value。
从使用方法上来讲,反射提供了一种机制,允许程序在运行时检查接口变量内部存储的 (value, type) 对。
在最开始,我们先了解下 reflect 包的两种类型 Type 和 Value,这两种类型使访问接口内的数据成为可能,它们对应两个简单的方法,分别是 reflect.TypeOf 和 reflect.ValueOf,分别用来读取接口变量的 reflect.Type 和 reflect.Value 部分。
当然,从 reflect.Value 也很容易获取到 reflect.Type,目前我们先将它们分开。
首先,我们下看 reflect.TypeOf:
func main() { var x float64 = 3.4 fmt.Println("type:", reflect.TypeOf(x)) }
运行结果如下
type: float64
大家可能会疑惑,为什么没看到接口?
这段代码看起来只是把一个 float64 类型的变量 x 传递给 reflect.TypeOf 并没有传递接口。
其实在 reflect.TypeOf 的函数签名里包含一个空接口:
// TypeOf returns the reflection Type of the value in the interface{}. func TypeOf(i interface{}) Type
我们调用 reflect.TypeOf(x) 时,x 被存储在一个空接口变量中被传递过去,然后 reflect.TypeOf 对空接口变量进行拆解,恢复其类型信息。
函数 reflect.ValueOf 也会对底层的值进行恢复:
func main() { var x float64 = 3.4 fmt.Println("value:", reflect.ValueOf(x)) }
运行结果如下:
value: 3.4
类型reflect.Type 和reflect.Value都有很多方法,我们可以检查和使用它们,这里我们举几个例子。
类型reflect.Value有一个方法Type(),它会返回一个reflect.Type 类型的对象。
Type 和 Value 都有一个名为Kind ()的方法,它会返回一个常量,表示底层数据的类型,常见值有:Uint、Float64、Slice 等。
Value 类型也有一些类似于 Int、Float 的方法,用来提取底层的数据:
Int 方法用来提取 int64
Float 方法用来提取 float64,示例代码如下:
func main() { var x float64 = 3.4 v := reflect.ValueOf(x) // 返回一个reflect.Value 类型的对象 fmt.Println("type:", v.Type()) // v.Type返回一个reflect.Type类型的对象 fmt.Println("kind is float64:", v.Kind() == reflect.Float64)// kind()返回一个常量,表示底层数据类型 fmt.Println("value:", v.Float()) // 提取底层的数据 }
运行结果如下:
type: float64 kind is float64: true value: 3.4
还有一些用来修改数据的方法,比如 SetInt、SetFloat。
在介绍它们之前,我们要先理解“可修性”(settability),这一特性会在下面进行详细说明。
反射库提供了很多值得列出来单独讨论的属性,下面就来介绍一下。
首先是介绍下 reflect.Value类型 的 getter 和 setter 方法,为了保证 API 的精简,这两个方法操作的是某一组类型范围最大的那个。比如,处理任何含符号整型数,都使用 int64,也就是说 Value 类型的 Int 方法返回值为 int64 类型,SetInt 方法接收的参数类型也是 int64 类型。实际使用时,可能需要转化为实际的类型:
func main() { var x uint8 = 'x' v := reflect.ValueOf(x) fmt.Println("type:", v.Type()) // uint8. fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) // true. x = uint8(v.Uint()) // v.Uint()返回的是uint64类型. }
运行结果如下:
type: uint8 kind is uint8: true
其次,反射对象的 Kind() 方法描述的是基础类型,而不是静态类型。如果一个反射对象包含了用户定义类型的值,如下所示:
func main() { type MyInt int var x MyInt = 7 v := reflect.ValueOf(x) fmt.Println(v.Kind()) // int fmt.Println(v.Type()) // main.MyInt }
上面的代码中,虽然变量 v 的静态类型是 MyInt,而不是 int,但 Kind 方法仍然会返回 reflect.Int。
换句话说 Kind ()方法不会像 Type() 方法一样区分 MyInt 和 int。
反射第二定律:反射可以将“反射类型对象”转换为“接口类型变量”
和物理学中的反射类似,Go语言中的反射也能创造自己反面类型的对象。
根据一个reflect.Value类型的变量,我们可以使用 Interface 方法恢复其接口类型的值。事实上,这个方法会把 type 和 value 信息打包并填充到一个接口变量中,然后返回。
其函数声明如下:
// Interface returns v's value as an interface{}. // 相当于:var i interface{}=(v的底层值)。 func (v Value) Interface() interface{}
然后,我们可以通过断言,恢复底层的具体值:
func main() { var x float64 = 12.1 v:=reflect.ValueOf(x) y,ok:= v.Interface().(float64) // y will have type float64. fmt.Println(ok,y)// true 12.1 }
上面这段代码会打印出一个 float64 类型的值,也就是反射类型变量 v 所代表的值。
事实上,我们可以更好地利用这一特性,标准库中的 fmt.Println 和 fmt.Printf 等函数都接收空接口变量作为参数,fmt 包内部会对接口变量进行拆包,因此 fmt 包的打印函数在打印 reflect.Value 类型变量的数据时,只需要把 Interface 方法的结果传给格式化打印程序:
fmt.Println(v.Interface())
那么为什么不直接使用 fmt.Println(v)?
因为 v 的类型是 reflect.Value,我们需要的是它的具体值,由于值的类型是 float64,我们也可以用浮点格式化打印它:
fmt.Printf("value is %7.1e\n", v.Interface())
运行结果如下:
3.4e+00
同样,这次也不需要对 v.Interface() 的结果进行类型断言,空接口值内部包含了具体值的类型信息,Printf 函数会恢复类型信息。
func main() { var x float64 = 12.1 v:=reflect.ValueOf(x) fmt.Printf("%T\n",v) // reflect.Value fmt.Printf("%T\n",v.Interface()) // float64 }
简单来说 Interface 方法和 ValueOf 函数作用恰好相反,唯一一点是,返回值的静态类型是 interface{}。
Go的反射机制可以将“接口类型的变量”转换为“反射类型的对象”,然后再将“反射类型对象”转换过去。
反射第三定律:如果要修改“反射类型对象”其值必须是“可写的”
这条定律很微妙,也很容易让人迷惑,但是如果从第一条定律开始看,应该比较容易理解。
下面这段代码虽然不能正常工作,但是非常值得研究:
var x float64 = 3.4 v := reflect.ValueOf(x) v.SetFloat(7.1) // Error: will panic
如果运行这段代码,它会抛出一个奇怪的异常:
panic: reflect: reflect.flag.mustBeAssignable using unaddressable value
这里问题不在于值7.1不能被寻址,而是因为变量 v 是“不可写的”,“可写性”是反射类型变量的一个属性,但不是所有的反射类型变量都拥有这个属性。
我们可以通过 CanSet() 方法检查一个 reflect.Value 类型变量的“可写性”,对于上面的例子,可以这样写:
func main() { var x float64 = 12.1 v:=reflect.ValueOf(x) fmt.Println("settability of v:", v.CanSet()) }
运行结果如下:
settability of v: false
对于一个不具有“可写性”的 Value 类型变量,调用 Set 方法会报出错误。
首先我们要弄清楚什么是“可写性”,“可写性”有些类似于寻址能力,但是更严格,它是反射类型变量的一种属性,赋予该变量修改底层存储数据的能力。“可写性”最终是由一个反射对象是否存储了原始值而决定的。
示例代码如下:
var x float64 = 3.4 v := reflect.ValueOf(x)
这里我们传递给 reflect.ValueOf 函数的是变量 x 的一个拷贝,而非 x 本身,想象一下如果下面这行代码能够成功执行:
v.SetFloat(7.1)
如果这行代码能够成功执行,它不会更新 x,虽然看起来变量 v 是根据 x 创建的,相反它会更新 x 存在于反射对象 v 内部的一个拷贝,而变量 x 本身完全不受影响。这会造成迷惑,并且没有任何意义,所以是不合法的。“可写性”就是为了避免这个问题而设计的。
这看起来很诡异,事实上并非如此,而且类似的情况很常见。考虑下面这行代码:
f(x)
代码中,我们把变量 x 的一个拷贝传递给函数,因此不期望它会改变 x 的值。如果期望函数 f 能够修改变量 x,我们必须传递 x 的地址(即指向 x 的指针)给函数 f,如下所示:
f(&x)
反射的工作机制与此相同,如果想通过反射修改变量 x,就要把想要修改的变量的指针传递给反射库。
首先,像通常一样初始化变量 x,然后创建一个指向它的反射对象,命名为 p:
func main() { var x float64 = 3.4 p := reflect.ValueOf(&x) // Note: take the address of x. fmt.Println("type of p:", p.Type()) fmt.Println("settability of p:", p.CanSet()) }
运行结果如下:
type of p: *float64 settability of p: false
反射对象 p 是不可写的,但是我们也不想修改 p,事实上我们要修改的是 *p。为了得到 p 指向的数据,可以调用 Value 类型的 Elem 方法。Elem 方法能够对指针进行“解引用”,然后将结果存储到反射 Value 类型对象 v 中:
func main() { var x float64 = 3.4 p := reflect.ValueOf(&x) // Note: take the address of x. v := p.Elem() fmt.Println("settability of v:", v.CanSet()) }
运行结果如下:
settability of v: true
由于变量 v 代表 x, 因此我们可以使用 v.SetFloat 修改 x 的值:
func main() { var x float64 = 3.4 p := reflect.ValueOf(&x) // Note: take the address of x. v := p.Elem() // 获取指针&x指向的变量x,等同于*(&x),但是在反射中不可以那么写,有一个专门的方法Elem()获取指针指向的变量 v.SetFloat(7.1) fmt.Println(v.Interface()) fmt.Println(x) }
运行结果如下:
7.1 7.1
Elem():专门用来获取指针指向的变量,返回仍然还是一个reflect.Vlaue类型
反射不太容易理解,reflect.Type 和 reflect.Value 会混淆正在执行的程序,但是它做的事情正是编程语言做的事情。只需要记住:只要反射对象要修改它们表示的对象,就必须获取它们表示的对象的地址。
结构体
我们一般使用反射修改结构体的字段,只要有结构体的指针,我们就可以修改它的字段。
下面是一个解析结构体变量 t 的例子,用结构体的地址创建反射变量,再修改它。然后我们对它的类型设置了 typeOfT,并用调用简单的方法迭代字段。
需要注意的是,我们从结构体的类型中提取了字段的名字,但每个字段本身是正常的 reflect.Value 对象。
func main() { type Student struct { Age int Name string } t := Student{19, "hyy"} s := reflect.ValueOf(&t).Elem() // 用结构体的地址创建反射变量s typeOfT := s.Type() fmt.Println(typeOfT) // main.Student // 迭代字段 // s.NumField() 返回结构体的字段个数 for i := 0; i < s.NumField(); i++ { f := s.Field(i) // 取第i个字段 fmt.Printf( "字段名:%s 字段类型:%s = %v\n", typeOfT.Field(i).Name, // 获取字段名 f.Type(), // 获取类型 f.Interface(), // 获取值 ) } }
运行结果如下:
main.Student 字段名:Age 字段类型:int = 19 字段名:Name 字段类型:string = hyy
Student 字段名之所以大写,是因为结构体中只有可导出的字段是“可设置”的。
// 如果设置成 type Student struct { Age int name string }
会报错painc:
因为 s 包含了一个可设置的反射对象,我们可以修改结构体字段:
func main() { type Student struct { Age int Name string } t := Student{19, "hyy"} s := reflect.ValueOf(&t).Elem() // 用结构体的地址创建反射变量s s.Field(0).SetInt(123) // t.Age=123 s.Field(1).SetString("123") // t.Name="123" fmt.Println("修改后:",t) }
运行结果如下:
修改后: {123 123}
如果我们修改了程序让 s 由 t(而不是 &t)创建,程序就会在调用 SetInt 和 SetString 的地方失败,因为 t 的字段是不可设置的。
总结
反射规则可以总结为如下几条:
反射可以将“接口类型变量”转换为“反射类型对象”;
反射可以将“反射类型对象”转换为“接口类型变量”;
如果要修改“反射类型对象”,其值必须是“可写的”。
二、reflect-反射类型对象-获取类型信息
1. TypeOf()和reflect.Type类型
通过反射获取类型信息
在 Go语言中通过调用 reflect.TypeOf() 函数,我们可以从一个任何非接口类型的值创建一个 reflect.Type 值。
reflect.Type 值表示着此非接口值的类型。通过此值,我们可以得到很多此非接口类型的信息。
当然,我们也可以将一个接口值传递给一个 reflect.TypeOf() 函数调用,但是此调用将返回一个表示着此接口值的动态类型的 reflect.Type 值。
实际上,reflect.TypeOf() 函数的唯一参数的类型为 interface{},reflect.TypeOf() 函数将总是返回一个表示着此唯一接口参数值的动态类型的 reflect.Type值。
那如何得到一个表示着某个接口类型的 reflect.Type 值呢?
我们必须通过下面将要介绍的一些间接途径来达到这一目的。
类型 reflect.Type 为一个接口类型,它指定了若干方法(Type)。
通过这些方法,我们能够观察到一个 reflect.Type 值所表示的 Go类型的各种信息。这些方法中的有的适用于所有种类(Kind)的类型,有的只适用于一种或几种类型。
通过不合适的 reflect.Type 值调用某个方法将在运行时产生一个painc。
使用 reflect.TypeOf() 函数可以获得任意值的类型对象(reflect.Type),程序通过类型对象可以访问任意值的类型信息。下面通过例子来理解获取类型对象的过程:
func main() { var a int // 通过 reflect.TypeOf() 取得变量 a 的类型对象 typeOfA,类型为 reflect.Type()。 typeOfA := reflect.TypeOf(a) // 通过 typeOfA 类型对象的成员函数, // 可以分别获取到 typeOfA 变量的类型名为 int,种类(Kind)为 int。 fmt.Println(typeOfA.Name(), typeOfA.Kind()) }
代码输出如下:
int int
1.1 理解反射的类型(Type)与种类(Kind)
在使用反射时,需要首先理解类型(Type)和种类(Kind)的区别。
编程中,使用最多的是类型,但在反射中,当需要区分一个大品种的类型时,就会用到种类(Kind)。
例如,需要统一判断类型中的指针时,使用种类(Kind)信息就较为方便。
总结:
type Student struct{...}
type 类型是Student
kind 种类是struct
1.1.1 反射种类(Kind)的定义
Go 程序中的类型(Type)指的是系统原生数据类型,如 int、string、bool、float32 等类型,以及使用 type 关键字定义的类型,这些类型的名称就是其类型本身的名称。
例如使用 type A struct{}定义结构体时,A 就是 struct{} 的类型。
种类(Kind)指的是对象归属的品种,在 reflect 包中有如下定义:
type Kind uint const ( Invalid Kind = iota // 非法类型 Bool // 布尔型 Int // 有符号整型 Int8 // 有符号8位整型 Int16 // 有符号16位整型 Int32 // 有符号32位整型 Int64 // 有符号64位整型 Uint // 无符号整型 Uint8 // 无符号8位整型 Uint16 // 无符号16位整型 Uint32 // 无符号32位整型 Uint64 // 无符号64位整型 Uintptr // 指针 Float32 // 单精度浮点数 Float64 // 双精度浮点数 Complex64 // 64位复数类型 Complex128 // 128位复数类型 Array // 数组 Chan // 通道 Func // 函数 Interface // 接口 Map // 映射 Ptr // 指针 Slice // 切片 String // 字符串 Struct // 结构体 UnsafePointer // 底层指针 )
Map、Slice、Chan 属于引用类型,使用起来类似于指针,但是在种类常量定义中仍然属于独立的种类,不属于 Ptr。
type A struct{} 定义的结构体属于 Struct 种类,*A 属于 Ptr。
1.1.2 从类型对象中获取类型名称和种类的例子
Go语言中的类型名称对应的反射获取方法是 reflect.Type 中的 Name() 方法,返回表示类型名称的字符串。
类型归属的种类(Kind)使用的是 reflect.Type 中的 Kind() 方法,返回 reflect.Kind 类型的常量。
下面的代码中会对常量和结构体进行类型信息获取。
// Enum 定义一个Enum类型 type Enum int const ( Zero Enum = 0 ) func main() { // 声明一个空结构体 type cat struct{} // 获取结构体实例的反射类型对象 typeOfCat := reflect.TypeOf(cat{}) // 显示反射类型对象的名称和种类 fmt.Println(typeOfCat.Name(), typeOfCat.Kind()) // 获取Zero常量的反射类型对象 typeOfA := reflect.TypeOf(Zero) // 显示反射类型对象的名称和种类 fmt.Println(typeOfA.Name(), typeOfA.Kind()) }
代码输出如下:
cat struct Enum int
1.2. reflect.Elem()
通过反射获取指针指向的元素类型。
func main() { // 声明一个空结构体 type cat struct { } // 创建cat的实例 ins := cat{} // 获取结构体实例的反射类型对象 typeOfCat := reflect.TypeOf(&ins) // 显示反射类型对象的名称和种类 fmt.Printf("name:'%v' kind:'%v'\n",typeOfCat.Name(), typeOfCat.Kind()) // 取类型的元素 typeOfCat = typeOfCat.Elem() // 显示反射类型对象的名称和种类 fmt.Printf("element name: '%v', element kind: '%v'\n", typeOfCat.Name(), typeOfCat.Kind()) }
对于指针一般都是直接
typeOfX := reflect.TypeOf(&x).Elem
取指针指向元素类型
1.3. 通过反射类型对象获取结构体的成员类型
任意值通过 reflect.TypeOf() 获得反射类型对象后,
如果它的类型是结构体,可以通过反射值对象(reflect.Type)的 NumField() 和 Field() 方法获得结构体成员的详细信息。
与成员获取相关的 reflect.Type 的方法如下表所示。
1.3.1 结构体字段类型(StructField )
reflect.Type 的 Field() 方法返回 StructField 结构,这个结构描述结构体的成员信息,通过这个信息可以获取成员与结构体的关系,如偏移、索引、是否为匿名字段、结构体标签(Struct Tag)等,而且还可以通过 StructField 的 Type 字段进一步获取结构体成员的类型信息。
StructField 的结构如下:
type StructField struct { Name string // 字段名 PkgPath string // 字段路径 Type Type // 字段反射类型对象 Tag StructTag // 字段的结构体标签 Offset uintptr // 字段在结构体中的相对偏移 Index []int // Type.FieldByIndex中的返回的索引值 Anonymous bool // 是否为匿名字段 }
字段说明如下。
Name:为字段名称。
PkgPath:字段在结构体中的路径。
Type:字段本身的反射类型对象,类型为 reflect.Type,可以进一步获取字段的类型信息。
Tag:结构体标签,为结构体字段标签的额外信息,可以单独提取。
Index:FieldByIndex 中的索引顺序。
Anonymous:表示该字段是否为匿名字段。
func main() { // 声明一个结构体 type cat struct { // 带有结构体tag的字段 Type int `json:"type" id:"100"` } // 创建cat的实例 ins := cat{Name: "mimi", Type: 1} // 获取结构体实例的反射类型对象 typeOfCat := reflect.TypeOf(&ins).Elem() // 2. 通过字段名, 找到字段类型信息 catType, ok := typeOfCat.FieldByName("Type") if ok { fmt.Println(catType.Name) //Type fmt.Println(catType.PkgPath) // "" fmt.Println(catType.Type) // int fmt.Println(catType.Tag) // json:"type" id:"100" fmt.Println(catType.Offset) // 16 fmt.Println(catType.Index) // [1] fmt.Println(catType.Anonymous) // false } }
1.3.2 获取成员反射信息
下面代码中,实例化一个结构体并遍历其结构体成员,再通过 reflect.Type 的 FieldByName() 方法查找结构体中指定名称的字段,直接获取其类型信息。
反射访问结构体成员类型及信息:
func main() { // 声明一个空结构体 type cat struct { Name string // 带有结构体tag的字段 Type int `json:"type" id:"100"` } // 创建cat的实例 ins := cat{Name: "mimi", Type: 1} // 获取结构体实例的反射类型对象 typeOfCat := reflect.TypeOf(&ins).Elem() // 方法1. 遍历结构体所有成员字段 for i := 0; i < typeOfCat.NumField(); i++ { // 获取每个成员的结构体字段类型 fieldType := typeOfCat.Field(i) // 输出成员名,成员反射类型和tag fmt.Printf("name: %v,type: %v,tag: '%v'\n", fieldType.Name,fieldType.Type, fieldType.Tag) } // 方法2. 通过字段名, 找到字段类型信息 catType, ok := typeOfCat.FieldByName("Type") if ok { // 从tag中取出需要的tag fmt.Println(catType.Tag.Get("json"), catType.Tag.Get("id")) } }
代码输出如下:
name: Name,type: string,tag: '' name: Type,type: int,tag: 'json:"type" id:"100"' type 100
1.4 通过反射类型对象创建实例
当已知 reflect.Type 时,可以动态地创建这个类型的实例,实例的类型为指针。
例如 reflect.Type 的类型为 int 时,创建 int 的指针,即*int
,代码如下:
func main() { var a int // 取变量a的反射类型对象 typeOfA := reflect.TypeOf(a) // 根据反射类型对象创建类型实例 // 使用 reflect.New() 函数传入变量 a 的反射类型对象,创建这个类型的实例值,值以 reflect.Value 类型返回。这步操作等效于:new(int),因此返回的是 *int 类型的实例。 aIns := reflect.New(typeOfA) // 输出Value的类型和种类 fmt.Println(aIns.Type(), aIns.Kind()) }
运行结果:
*int ptr
reflect-反射值对象-获取值信息