教程集 www.jiaochengji.com
教程集 >  Golang编程  >  golang教程  >  正文 《GO语言圣经》读书笔记(十一):反射

《GO语言圣经》读书笔记(十一):反射

发布时间:2023-03-16   编辑:jiaochengji.com
教程集为您提供《GO语言圣经》读书笔记(十一):反射等资源,欢迎您收藏本站,我们将为您提供最新的《GO语言圣经》读书笔记(十一):反射资源

文章目录

  • 变量的内在机制
  • 为什么需要反射
  • reflect包
    • Type和Value
    • TypeOf
      • type name和type kind
    • ValueOf
      • 通过反射获取值
      • 通过反射设置值
      • isNil()和isValid()
        • isNil()
        • isValid()
        • 举个例子
  • 结构体反射
    • 与结构体相关的方法
  • 反射三大定律
    • 第一条定律
    • 第二条定律
    • 第三条定律
  • 为什么我们吐槽反射太慢
  • 进击的反射
  • 小结

本节读书笔记对应原书第十二章。

变量的内在机制

​ Go语言中的变量是分为两部分的:

  • 类型信息:预先定义好的元信息。
  • 值信息:程序运行过程中可动态变化的。

​ 还有个经常容易混的,在这里也单独记一遍:值类型和引用类型。

  • 所有像 int、float、bool 和 string ,数组,struct这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值;
  • ptr,函数,映射,slice,map,chan更复杂的数据通常会需要使用多个字,这些数据一般使用引用类型保存。

为什么需要反射

​ 想想这样一个场景,需要我们实现这样一个功能:测试输入参数是否实现了String方法,如果是的话,使用该方法,那很容易想到通过switch去枚举输入参数可能的类型,虽然可以添加更多的case,但是类型组合是很多的,而且这种方式没办法应对未知类型。

​ 但是如果使用反射,那么我们可以在程序编译期将变量的反射信息,字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。

在Go中,我们通过反射可以访问程序中的变量,还可以通过反射机制来修改变量。

reflect包

​ 在Go语言的反射机制中,任何接口值都由是一个具体类型具体类型的值两部分组成的。 在Go语言中反射的相关功能由内置的reflect包提供,任意接口值在反射中都可以理解为由reflect.Typereflect.Value两部分组成,并且reflect包提供了reflect.TypeOfreflect.ValueOf两个函数来获取任意对象的Value和Type。

Type和Value

​ 作为反射包中重要的两个类型,我们先来看看是什么吧。

Type其实是反射包中的一个接口,在这个接口中定义了一些方法,如下所示:

type Type interface {
        Align() int
        FieldAlign() int
        Method(int) Method
        MethodByName(string) (Method, bool)
        NumMethod() int
        ...
        Implements(u Type) bool
        ...
}

​ 而反射包中的Value并不是一个接口,而是被声明成了一个结构体,请注意,这个结构体中的字段不对外暴露(小写字母开头表示不对外暴露,不可导出),为了方便访问和修改,提供了获取/写入数据的方法:

type Value struct {
    	typ *rtype
		ptr unsafe.Pointer
		flag
}

// Addr returns a pointer value representing the address of v.
// It panics if CanAddr() returns false.
// Addr is typically used to obtain a pointer to a struct field
// or slice element in order to call a method that requires a
// pointer receiver.
func (v Value) Addr() Value {
    //... ...
}

// Bool returns v's underlying value.
// It panics if v's kind is not Bool.
func (v Value) Bool() bool {
    //... ...
}

// Bytes returns v's underlying value.
// It panics if v's underlying value is not a slice of bytes.
func (v Value) Bytes() []byte {
    //... ...
}

​ 目前并不需要太在意这些类型的内部实现,简单介绍这些只是为后面的两个函数做铺垫。

TypeOf

​ 在Go语言中,使用reflect.TypeOf()函数可以获得任意值的类型对象(reflect.Type),并返回对应动态类型的reflect.Type

​ 下面的例子中简单演示了如何使用reflect.TypeOf,以第一个小例子来说,我们将值3作为参数传入reflect.TypeOf()中,注意在这里将会有一个隐式的接口转换操作,为啥呢,TypeOf()为了接受各种类型的参数,设置成了interface{}类型,所以这里会把一个具体的值转化为空接口。

t := reflect.TypeOf(3) // a reflect.Type
fmt.Println(t.String()) // "int"
fmt.Println(t) // "int"


type test int32
var pp test = 33
t := reflect.TypeOf(pp) // a reflect.Type
fmt.Println(t.String()) // "main.test"
fmt.Println(t)          // "main.test"

var w io.Writer = os.Stdout
fmt.Println(reflect.TypeOf(w)) // "*os.File

​ 上面一共列举了三个例子:

  • 第一个例子中,没有什么花里胡哨的,获取一个简单的类型int,这没有什么难度;
  • 第二个例子中,通过type关键字定义了一个类型别名test,实际上底层的类型是int32,在这里具体的类型是test
  • 第三个例子,io.Writer是一个接口提供了write方法,os.Stdout实现了该接口的所有方法,所以你可以看到声明w变量是合法的(这有点像是Java的面向接口编程…),但是有很多类型都实现了io.Writer接口,在这里,动态类型指的是os.Stdout而不是静态类型io.Writer

​ 到这里应该对这句话有一点点体会了:reflect.TypeOf返回的是一个动态类型的接口值,它总是返回具体的类型。

type name和type kind

​ 在反射中关于类型还划分为两种:类型(Type)种类(Kind)。因为在Go语言中我们可以使用type关键字构造很多自定义类型,而种类(Kind)就是指底层的类型,但在反射中,当需要区分指针、结构体等大品种的类型时,就会用到种类(Kind)。 举个例子,我们定义了两个指针类型和两个结构体类型,通过反射查看它们的类型和种类。

package main

import (
	"fmt"
	"reflect"
)

type myInt int64

func reflectType(x interface{}) {
	t := reflect.TypeOf(x)
	fmt.Printf("type:%v kind:%v\n", t.Name(), t.Kind())
}

func main() {
	var a *float32 // 指针
	var b myInt    // 自定义类型
	var c rune     // 类型别名
	reflectType(a) // type: kind:ptr
	reflectType(b) // type:myInt kind:int64
	reflectType(c) // type:int32 kind:int32

	type person struct {
		name string
		age  int
	}
	type book struct{ title string }
	var d = person{
		name: "哭唧唧哇",
		age:  18,
	}
	var e = book{title: "123不许动"}
	reflectType(d) // type:person kind:struct
	reflectType(e) // type:book kind:struct
}

​ Go语言的反射中像数组、切片、Map、指针等类型的变量,它们的.Name()都是返回

​ 在reflect包中定义的Kind类型如下:

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        // 底层指针
)

ValueOf

​ 除了TypeOf很重要之外,ValueOf也不容忽视。reflect.ValueOf接受任意的interface{}类型,并返回动态类型的接口值的具体值。

​ reflect.Value 也满足 fmt.Stringer 接口,但是如果Value是个字符串,那么String返回的是值,其他情况返回的是具体的类型。

t := reflect.TypeOf(3)   // a reflect.Type
fmt.Println(t.String())  // "int"

v := reflect.ValueOf(3)  // a reflect.Value
fmt.Println(v.String())  // NOTE: "<int Value>"

q := reflect.ValueOf("2333")  
fmt.Println(q.String())  //"2333"
 下面这张图展示了`Type`、`Value`、`Interface`三者之间的转化关系,简单用文字来描述下:
  • 调用ValueType()方法将返回具体类型所对应的reflect.Type

  • 调用reflect.Value.Interface方法,将会返回一个interface{}类型,表示reflect.Value对应类型的具体值;

    v := reflect.ValueOf(3) // a reflect.Value
    x := v.Interface() // an interface{},直接打印x的结果其实也是3,只不过区别于变量i的不同之处是类型不同
    i := x.(int) // an int 这里将空接口还原成最原始的状态,必须使用显式的类型转换
    fmt.Printf("%d\n", i) // "3"
    

    虽然reflect.Value和空接口类型都可以保存值,但是空接口隐藏值对应的表达方式和公开方法,我们没法利用它去做什么事情,Value却有很多方法供我们使用。

reflect.Value类型提供的获取原始值的方法如下:

方法说明
Interface() interface {}将值以 interface{} 类型返回,可以通过类型断言转换为指定类型
Int() int64将值以 int 类型返回,所有有符号整型均可以此方式返回
Uint() uint64将值以 uint 类型返回,所有无符号整型均可以此方式返回
Float() float64将值以双精度(float64)类型返回,所有浮点数(float32、float64)均可以此方式返回
Bool() bool将值以 bool 类型返回
Bytes() []bytes将值以字节数组 []bytes 类型返回
String() string将值以字符串类型返回

通过反射获取值

func reflectValue(x interface{}) {
	v := reflect.ValueOf(x)
	k := v.Kind()
	switch k {
	case reflect.Int64:
		// v.Int()从反射中获取整型的原始值,然后通过int64()强制类型转换
		fmt.Printf("type is int64, value is %d\n", int64(v.Int()))
	case reflect.Float32:
		// v.Float()从反射中获取浮点型的原始值,然后通过float32()强制类型转换
		fmt.Printf("type is float32, value is %f\n", float32(v.Float()))
	case reflect.Float64:
		// v.Float()从反射中获取浮点型的原始值,然后通过float64()强制类型转换
		fmt.Printf("type is float64, value is %f\n", float64(v.Float()))
	}
}
func main() {
	var a float32 = 3.14
	var b int64 = 100
	reflectValue(a) // type is float32, value is 3.140000
	reflectValue(b) // type is int64, value is 100
	// 将int类型的原始值转换为reflect.Value类型
	c := reflect.ValueOf(10)
	fmt.Printf("type c :%T\n", c) // type c :reflect.Value
}

通过反射设置值

​ 一个变量是一个可寻址的内存空间(前几章提到过这个概念),我们可以通过内存地址来更新变量存储的值。那么对于有一些reflect.Values来说,也是可以取地址的,其他一些是不可以的。

​ 想要在函数中通过反射修改变量的值,需要注意函数参数传递的是值拷贝,必须传递变量地址才能修改变量值。下面举了四个小例子,来进一步说哪些是可以取地址,哪一些是不可以取的:

x := 2 					// value type variable?
a := reflect.ValueOf(2) // 2     int 	no
b := reflect.ValueOf(x) // 2	 int 	no
c := reflect.ValueOf(&x) // &x   *int 	no
d := c.Elem() 			// 2      int   yes (x)

​ 变量a/b/c中的值都是不可取地址,咦,为什么c明明传入了一个指针,这也不可以?ab变量中传入的都是整数2的拷贝副本,所以值不可取,对于c来说,实际上只是一个指针&x的拷贝,所以这就解释了为什么我们说参数传递的是值拷贝。

​ 反射中使用专有的Elem()方法来获取指针对应的值,所以可以使用reflect.ValueOf(&x).Elem()来获取任意x变量对应的可取地址的Value。还可以通过调用reflect.ValueCanAddr方法来判断其是否可以被取地址(不过等会就会发现这个方法也有不太灵活的一面):

fmt.Println(a.CanAddr()) // "false"
fmt.Println(b.CanAddr()) // "false"
fmt.Println(c.CanAddr()) // "false"
fmt.Println(d.CanAddr()) // "true"

​ 看一个实际的例子:

package main

import (
	"fmt"
	"reflect"
)

func reflectSetValue1(x interface{}) {
	v := reflect.ValueOf(x)
	if v.Kind() == reflect.Int64 {
		v.SetInt(200) //修改的是副本,reflect包会引发panic
	}
}
func reflectSetValue2(x interface{}) {
	v := reflect.ValueOf(x)
	// 反射中使用 Elem()方法获取指针对应的值
	if v.Elem().Kind() == reflect.Int64 {
		v.Elem().SetInt(200)
	}
}
func main() {
	var a int64 = 100
	// reflectSetValue1(a) //panic: reflect: reflect.Value.SetInt using unaddressable value
	reflectSetValue2(&a)
	fmt.Println(a)
}

​ 在上面的例子中,我们通过调用可取地址的reflect.Valuereflect.Value.Set方法进行设置值(更新值也是可以的)。reflect.Value.Set方法在更新值之前,会分别检查反射对象是否可以被设置以及字段是否是对外公开的,方法中会调用reflect.Value.assignTo()返回一个新的反射对象,该对象的指针会覆盖原来的反射变量,达到更新的效果。

​ 这里无论是在reflectSetValue2还是reflectSetValue中,设置200值的时候,我们通过的是SetInt,只要变量是某种类型的有符号整数就可以,如果一个引用interface{}类型的reflect.Value调用SetInt会导致panic异常,即使interface{}变量是个整数类型也不行。实际上还有很多用于基本数据类型的Set方法,比如SetUint/SetString等等。

​ 但是要注意:

  • 对于一个不可取地址的reflect.Value调用Set方法也会导致panic异常;
  • 要保证变量可以接受对应类型的值,不可以int64的值赋给int型变量,那样会抛出panic

​ 我们通过反射可以越过go语言导出规则的限制读取结构体中没有导出的成员,但是利用反射机制是不可以修改未导出的成员!

​ 我们之前说过有一些reflect.Value是可以取地址的,如果一个结构体成员属于未导出的,却又可以取地址,那么这种情况下可以修改它的值吗?

​ 当然不行…一个可取地址的reflect.Value会记录一个结构体成员是否为未导出成员,如果是那就拒绝修改操作,刚刚提到了一个CanAddr方法可以判断一个变量是否可以取地址,对于这种未导出成员,调用该方法并不能确定它是否可以被修改,CanSet既可以检查是否可以取地址也可以检查是否可以被修改。

isNil()和isValid()

isNil()

func (v Value) IsNil() bool

IsNil()报告v持有的值是否为nil。v持有的值的分类必须是通道、函数、接口、映射、指针、切片之一;否则IsNil函数会导致panic。

isValid()

func (v Value) IsValid() bool

IsValid()返回v是否持有一个值。如果v是Value零值会返回假,此时v除了IsValid、String、Kind之外的方法都会导致panic。

举个例子

IsNil()常被用于判断指针是否为空;IsValid()常被用于判定返回值是否有效。

func main() {
	// *int类型空指针
	var a *int
	fmt.Println("var a *int IsNil:", reflect.ValueOf(a).IsNil())
	// nil值
	fmt.Println("nil IsValid:", reflect.ValueOf(nil).IsValid())
	// 实例化一个匿名结构体
	b := struct{}{}
	// 尝试从结构体中查找"abc"字段
	fmt.Println("不存在的结构体成员:", reflect.ValueOf(b).FieldByName("abc").IsValid())
	// 尝试从结构体中查找"abc"方法
	fmt.Println("不存在的结构体方法:", reflect.ValueOf(b).MethodByName("abc").IsValid())
	// map
	c := map[string]int{}
	// 尝试从map中查找一个不存在的键
	fmt.Println("map中不存在的键:", reflect.ValueOf(c).MapIndex(reflect.ValueOf("娜扎")).IsValid())
}

结构体反射

与结构体相关的方法

任意值通过reflect.TypeOf()获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象(reflect.Type)的NumField()Field()方法获得结构体成员的详细信息。

reflect.Type中与获取结构体成员相关的的方法如下表所示。

方法说明
Field(i int) StructField根据索引,返回索引对应的结构体字段的信息。
NumField() int返回结构体成员字段数量。
FieldByName(name string) (StructField, bool)根据给定字符串返回字符串对应的结构体字段的信息。
FieldByIndex(index []int) StructField多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息。
FieldByNameFunc(match func(string) bool) (StructField,bool)根据传入的匹配函数匹配需要的字段。
NumMethod() int返回该类型的方法集中方法的数目
Method(int) Method返回该类型方法集中的第i个方法
MethodByName(string)(Method, bool)根据方法名返回该类型方法集中的方法

反射三大定律

​ 写到这里,反射这一章的内容就要接近尾声了。在了解很多反射小知识之后,再去理解Go官方博客中的反射三大定律就会容易很多:

1.Reflection goes from interface value to reflection object.

翻译:interface{}变量可以转化成反射对象;

2.Reflection goes from reflection object to interface value.

翻译:从反射对象可以获取interface{}变量;

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

翻译:要修改反射对象,那么它的值必须是可设置的。

第一条定律

​ 如果你仔细看上面我写的内容,会发现不管是在介绍ValueOf还是TypeOf函数的时候,都提到了类型隐式转换为interface{}类型。reflect.ValueOf(x)中(假设x:=2)的x作为函数的入参,函数调用时会转换成空接口类型,在TypeOfValueOf函数内部,完成了空接口类型转化为reflect.Value类型或者是reflect.Type类型的工作,这就是第一条定律要表达的内容。

​ 通过这两个函数就可以将反射类型和Go的数据类型关联起来。

第二条定律

​ 第二条定律也很好理解。在介绍ValueOf的时候,通过一张图梳理了以下各部分之间的转化关系。

reflect.Value类型的Interface方法就可以实现从反射对象到interface{}之间的转化,但是这个转化获得的变量类型是空接口类型,如果想要还原成最原始的状态,那么需要进行显示转化(上面也有例子噢)。

​ 但是不管从接口值到反射对象,还是从反射对象到接口值,都会经历两次转换,不过需要说明的是,如果变量本身就是空接口类型,那就不需要类型转化了(虽然这个过程是隐式的):

  • 从接口值到反射对象:
    • 从基本类型到接口类型的类型转换;
    • 从接口类型到反射对象的转换;
  • 从反射对象到接口值:
    • 反射对象转换成接口类型;
    • 通过显式类型转换变成原始类型;

第三条定律

​ 前两条还比较好理解,通过TypeOfValueOf函数可以实现第一条,实现第二条就可以用前面提到的Value类型结构体中的interface()方法实现逆转化。

​ 第三条耗尽洪荒之力可能也不太好理解了,典型明明英文读的懂就是不知道它在说啥。官方啥意思呢,先翻译一下就是你要是想操作反射变量,那么这个值必须是可设置的。啥叫可设置的呢,我们原来有个变量A(就是原变量),然后通过反射拿到了它对应的反射变量B,我们对变量B的修改同时会影响原变量A,那么这就属于可设置的。

​ 如果使用反射的时候,不清楚这一点,直接取修改反射对象,那么就要恭喜你喜提panic一次了。

func main() {
	i := 1
	v := reflect.ValueOf(i)
	v.SetInt(10)
	fmt.Println(i)
}

输出:

[root@cdb63e69049f ~/go/src/MathDemoServer/MathDemoServer]# go run main.go
panic: reflect: reflect.flag.mustBeAssignable using unaddressable value

goroutine 1 [running]:
reflect.flag.mustBeAssignableSlow(0x82)
        /usr/local/go/src/reflect/value.go:247  0x138
reflect.flag.mustBeAssignable(...)
        /usr/local/go/src/reflect/value.go:234
reflect.Value.SetInt(0x49e860, 0xc000016088, 0x82, 0xa)
        /usr/local/go/src/reflect/value.go:1606  0x3b
main.main()
        /root/go/src/MathDemoServer/MathDemoServer/main.go:11  0xb2
exit status 2

​ 所以咋整呢,只需要三步:首先获取变量指针,其次获取指针指向的变量,最后更新变量的值。这三步分别对应三个函数:

  • 调用 reflect.ValueOf函数获取变量指针;

  • 调用 reflect.Value.Elem方法获取指针指向的变量;

  • 调用 reflect.Value.SetInt 方法更新变量的值。

    上面的内容列举了例子啊,如果忘记了就往上翻翻。

为什么我们吐槽反射太慢

​ 在一些博客中看到这样一段话(大意是反射效率不高),照着Golang反射性能分析这篇博客简单测了一下,确实反射挺慢…

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

进击的反射

这部分就有点难理解了…所以掉了好多头发…现在不太理解也没事,跳过去就好了,会看懂的。

​ Go 语言的 interface{} 类型在语言内部是通过 emptyInterface 这个结构体来表示的,其中的 rtype 字段用于表示变量的类型,另一个 word 字段指向内部封装的数据:

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

​ 用于获取变量类型的 reflect.TypeOf函数将传入的变量隐式转换成空接口interface{}类型,然后通过底层编程的一些方法转化成emptyInterface类型,赋值给eface, 最后返回其中存储的类型信息 rtype

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

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

rtype 就是一个实现了 Type 接口的结构体,我们能在 reflect包中找到reflect.rtype.String 方法帮助我们获取当前类型的名称等信息:

func (t *rtype) String() string {
	s := t.nameOff(t.str).name()
	if t.tflag&tflagExtraStar != 0 {
		return s[1:]
	}
	return s
}

reflect.TypeOf 函数的实现原理其实并不复杂,它只是将一个 interface{} 变量转换成了内部的 emptyInterface 表示,然后从中获取相应的类型信息。

​ 用于获取接口值 Value 的函数 reflect.ValueOf 实现也非常简单。reflect.unpackEface 函数会将传入的接口转换成 emptyInterface 结构体,然后将具体类型和指针包装成 Value 结构体并返回:

func ValueOf(i interface{}) Value {
	if i == nil {
		return Value{}
	}

	//......

	return unpackEface(i)
}

func unpackEface(i interface{}) Value {
	e := (*emptyInterface)(unsafe.Pointer(&i))
	t := e.typ
	if t == nil {
		return Value{}
	}
	f := flag(t.Kind())
	//... ...
	return Value{t, e.word, f}
}

​ 当我们想要将一个变量转换成反射对象时,Go 语言会在编译期间完成类型转换的工作,将变量的类型和值转换成了 interface{} 并等待运行期间使用 reflect包获取接口中存储的信息。

小结

​ 虽然在大多数的应用和服务中并不常见到使用反射(可能是因为大量使用反射代码比较难懂、以及性能比较差吧),但是很多框架都依赖反射机制去实现简化代码逻辑的工作(比如开源的gorm框架就用到了反射去处理数据库表字段和表结构体的一一对应的关系)。

reflect包中最重要的2个函数就是reflect.TypeOfreflect.ValueOf,前者完成了获取类型信息的工作,知道了变量的类型之后,我们可以获得类型实现的方法,获取类型包含的全部字段,对于不同的数据结构,可以获得的信息不同:

[关闭]
~ ~