golang中反射的Q&A

反射的 Q&A

概念

  1. 在go语言中,reflect可以用来在运行期间获取对象的类型信息和内存结构,reflect操作所需的全部信息都来自接口变量
    接口变量除了保存自身的类型以外,还会保存实际对象的类型数据,reflect通过TypeOf函数和ValueOf函数,
    将任何传入的对象都转换为接口类型
  2. 使用反射获取对象类型
    func TypeOf(i interface{}) Type {
    获取对象的类型信息
    type A int

func main() {
	var a A = 1
	t := reflect.TypeOf(a)
	// Name返回具体类型(动态类型),Kind返回它的基础类型
	fmt.Println(t.Name(), t.Kind())  // A int
}

需要注意的是,Name是真实类型,Kind是真实类型的基础类型,在类型判断时要区分
传入的参数对象区分基本类型和指针类型

func main() {
	a := 1
	t1, t2 := reflect.TypeOf(a), reflect.TypeOf(&a)

	fmt.Println(t1, t2, t1 == t2, t2.Elem())  // int *int false int
	fmt.Println(t1.Kind(), t2.Kind())  // int ptr
	fmt.Println(t1 == t2.Elem())  // true
}

代码中我们使用Elem方法返回指针的基类型,此外Elem方法还可以返回数组、slice、map、channel的基类型

func main() {
	var b = make(chan int)
	t1, t2 := reflect.TypeOf(b), reflect.TypeOf(&b)
	fmt.Println(t1, t2, t1 == t2, t2.Elem())  // chan int  *chan int  false  chan int
	fmt.Println(t1.Kind(), t2.Kind())  // chan  ptr
	fmt.Println(t1 == t2.Elem())  // true
}

使用反射操作struct

  • 遍历结构体
  1. 遍历结构体字段,需要先获取结构体指针的基类型
func main() {
	s := student{name: "ma", age: 18}
	// 遍历结构体字段,需要先获取结构体指针的基类型
	t1 := reflect.TypeOf(&s)
	if t1.Kind() == reflect.Ptr{
		t1 = t1.Elem()  // 将结构体指针类型转换为结构体基类型
	}

	fmt.Println(t1.NumField())
	for i := 0; i < t1.NumField(); i++{
		structField := t1.Field(i)
		fmt.Println(structField.Name, structField.Type)
	}

}

输出结果:

2
name string
age int
  • 提取struct tag(结构体标签)
  1. 一般用于orm映射,数据格式验证
func main() {
	u := user{name: "ma", age: 18}

	t1 := reflect.TypeOf(u)

	for i := 0; i < t1.NumField(); i++{
		structField := t1.Field(i)
		fmt.Println(structField.Name, structField.Tag.Get("form"))
	}

}

输出结果:

name user_name
age user_age
  • 使用反射获取和修改对象的值
    func ValueOf(i interface{}) Value {
type Cat struct {
	Name string
	Age int
}

func main() {
	var c Cat
	v1 := reflect.ValueOf(&c).Elem()

	fmt.Println(v1, v1.Type(), v1.Kind())  // { 0} main.cat struct

	// 注意字段名必须大写
	v1.FieldByName("Name").SetString("汤姆")
	v1.FieldByName("Age").SetInt(10)

	fmt.Println(v1, v1.Type(), v1.Kind())  // {汤姆 10} main.Cat struct

}

通过代码可以看出ValueOf可以获取对象的值
我们知道,修改对象的变量,我们需要传入指针,但是因为接口变量存储的指针是不可以寻址和设置变量的,
所以我们需要通过Elem获取目标对象,非导出字段的值不能修改

type Cat struct {
	Name string
	Age int
}

func main() {
	var c Cat
	v1 := reflect.ValueOf(&c).Elem()  // 此处必须使用指针然后获取Elem(),如果直接传值也不能修改

	fmt.Println(v1, v1.Type(), v1.Kind())  // { 0} main.cat struct

	// 注意字段名必须大写,因为非导出字段的值不能被修改
	v1.FieldByName("Name").SetString("汤姆")
	v1.FieldByName("Age").SetInt(10)

	fmt.Println(v1, v1.Type(), v1.Kind())  // {汤姆 10} main.Cat struct

}
  • 使用反射动态调用方法
  1. 固定参数,使用call,只需要按照参数列表传递即可
type Member struct {}
func (m Member) MemberInfo(name string, score int) string {
	mInfo := fmt.Sprintf("%s考试的了%d分", name, score)
	return mInfo
}

func main() {
	var m Member

	v1 := reflect.ValueOf(m)
	param := []reflect.Value{
		reflect.ValueOf("哈哈"),
		reflect.ValueOf(88),
	}

	ret := v1.MethodByName("MemberInfo").Call(param)
	for _, v := range ret{
		fmt.Println(v)
	}

}

输出结果:哈哈考试的了88分
2. 可变参数使用callSlice,仅需一个[]interface{}即可

type Member struct {}
func (m Member) MemberInfo(name string, hobby... interface{}) string {
	mInfo := fmt.Sprintf("%s的爱好有%v", name, hobby)
	return mInfo
}

func main() {
	var m Member

	v1 := reflect.ValueOf(&m)

	param := []reflect.Value{
		reflect.ValueOf("lisi"),
		reflect.ValueOf([]interface{}{
			"pingpong", "篮球",
		}),
	}

	ret := v1.MethodByName("MemberInfo").CallSlice(param)
	for _, v := range ret{
		fmt.Println(v)
	}

}

输出结果:lisi的爱好有[pingpong 篮球]

go中反射有哪些应用?

  1. go语言中反射的应用非常广泛,IDE中的代码自动补全功能,对象序列化(encoding/json),fmt相关函数实现
    ORM(对象关系映射),数据的校验等

go中如何实现反射

  1. interface是go语言实现抽象的一个非常强大的工具,当向接口变量赋予一个实体类型的时候,接口会存储实体的类型信息
    反射就是通过接口的类型信息实现的,反射建立在类型的基础上
  2. 在reflect包里定义了各种类型,实现了反射的各种函数,通过它们可以在运行时检测类型信息,改变类型的值
types和interface
  1. go语言中,每一个变量都有一个静态类型,在编译阶段就确定了,例如int float64 []int类型,
    注意这个类型是声明时候的类型,不是底层的数据类型,例子
type MyInt int

func main() {
	var i int
	var j MyInt
}

尽管i,j底层的数据类型都是int, 但是我么知道,它们是不同的静态类型,除非进行类型转换,否则i和j不能出现在等号两侧
i的静态类型是int, j的静态类型是MyInt
2. 反射主要与interface{}类型相关,看一下interface的底层结构

type iface struct {
	tab  *itab
	data unsafe.Pointer
}
type itab struct {
	inter *interfacetype
	_type *_type
	hash  uint32 // copy of _type.hash. Used for type switches.
	_     [4]byte
	fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}

其中itab是由具体类型_type和接口类型interfacetype,还有func字段一个元素的指针数组,里面存储的是实际对象的方法地址
iface结构体描述的是非空接口,它包含方法,eface描述的是非空接口,不包含任何方法,go语言中所有的类型都实现了空接口

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

相比iface,eface就比较简单了,只维护了_type字段保存赋值给接口变量的实体类型,data描述了具体的值
想成为技术专家,读英文原始资料是技术提高的一条必经之路
3. 先明确一点,接口变量能存储任何实现了接口中所有方法的变量
最常见的就是Reader接口和Writer接口

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

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

func main() {
	var r io.Reader
	file, _ := os.OpenFile("test/abc.txt", os.O_RDWR, 0)
	r = file
}

首先声明r的类型是io.Reader,注意这是它的静态类型,此时它的动态类型为nil, 动态值也是nil
之后,r = file, 此时r的动态类型为os.File指针类型,动态值为非空表示打开的文件对象,
img_1.png
注意看上图,func所指向的函数只有一个Read函数,其实
os.File还包含Write函数,
也就是说*os.File也实现了Writer接口,因此下面的断言可以执行

func main() {
	var r io.Reader
	file, _ := os.OpenFile("test/abc.txt", os.O_RDWR, 0)
	r = file

	var w io.Writer
	//w = r  // 报错,因为r的静态类型是io.Reader, w的静态类型是io.Writer,静态类型不一样,不能直接赋值

	// 但是可以通过断言的方式转换类型在赋值
	w = r.(io.Writer)
	fmt.Println(w)
}

之所以用断言而不能直接赋值,是因为r的静态类型是io.Reader, r并没有实现io.Writer接口,所以不能直接赋值给w
断言能否成功,就看r的动态类型是否实现了io.Writer接口,如果实现了断言成功,否则失败
这样,w 也可以表示成 <tty, *os.File>,仅管它和 r 一样,但是 w 可调用的函数取决于它的静态类型 io.Writer,
也就是说它只能有这样的调用形式: w.Write() 。w 的内存形式如下图:
img_2.png
w和r相比,仅仅是func函数变量,Read --> Write, 其它都一样,

	var empty interface{}
	empty = w
	fmt.Println(empty)

由于empty是一个空接口,所以所有类型都实现了它,w可以直接赋值给他,不需要任何断言操作
img_3.png
从上面三张图可以看出,interface包含三部分,一个是itab中的interfacetype是接口类型信息,一个是data指向实际类型的实际值,
itab中的func字段一个元素的数组包含实际类型的方法地址

  • 注意:两个接口变量进行赋值的时候,它们的静态类型会发生变化,动态类型和动态值一直保持不变的
反射的基本函数
  1. reflect包里定义了一个接口和一个结构体,reflect.Type接口, reflect.Value结构体,
    它们提供很多函数来获取存储在接口里的类型信息
  2. reflect.Type主要提供关于类型相关的信息,所以它和_type关联比较紧密,而reflect.Value则结合_type和data两者,
    因此程序员可以获取甚至改变类型的值
  3. 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))
	return toType(eface.typ)
}

注意返回值Type是一个接口,里面包含了很多方法,用来获取类型相关的各种信息,看reflect.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
    // 返回 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定义了非常多的方法,通过她们可以获取类型的一切信息,所有类型都会包含rtype这个字段,表示各种类型的公共信息
注意,Type接口实现了String()函数,满足fmt.Stringer接口,因此使用fmt.Println()打印的时候,输出的是Strint()的结果,
另外fmt.Printf()如何使用%T作为格式化参数,输出的是reflect.TypeOf结果,也就是动态类型

ValueOf函数
  1. reflect.Value返回的是interface{}存储的实际变量,它能提供实际变量的各种信息,
    相关方法常常需要结合类型信息和值信息,
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结构体
Value包含类型结构体指针,真实数据的地址,标志位
2. 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

通过Type()方法和Interface()方法可以打通interface Type Value三者,Type()方法也可以返回变量的类型信息
与reflect.TypeOf()函数等价,Interface()方法可以将Value还原成原来的interface
img_4.png

  • 总结一下:TypeOf()函数返回一个Type接口,该接口定义而一系列方法,利用这些方法可以获取有关类型的所有信息
  • ValueOf()函数返回一个Value结构体变量, 包含类型信息以及实际值
    img_5.png
反射的三大定律(更具官方的说法)
  1. 第一条是:反射用来检测存储在interface中的类型和值的机制,通过reflect包中的TypeOf函数和ValueOf函数得到
  2. 第二条实际上是和第一条是相反的机制,它将ValueOf函数的返回值Value结构体变量通过Interface()方法转换成interface接口变量
  • 两条就是说接口变量可以和反射类型对象相互转化,反射类型的对象指的就是reflect.Type和reflect.Value两种类型
  1. 第三条不太好懂,如果想要操作一个反射变量,那么它必须是可设置的,反射变量可设置的本质就是它存储了原变量本身,
    这样对反射变量的操作就会反应到原变量本身,反之,如果反射变量不能代表原变量本身,那么操作了反射变量,
    不会对原变量产生任何影响,这样会给使用者带来疑惑,所以语言层面禁用了第二种情况
func main() {
	var a float64 = 3.14
	v := reflect.ValueOf(a)
	v.SetFloat(3.55)
}

报错:panic: reflect: reflect.Value.SetFloat using unaddressable value
为什么会报panic,原因是v不能代表a本身,因为调用reflect.ValueOf(a)时,传入的参数在函数内部是一个拷贝
是值传递,所以v代表的只是x的一个拷贝,因此对v进行操作是被禁止的
4. 可设置是反射变量Value的一个性质,但不是所有的Value都可以被设置
就像在一般的函数里那样,我们想改变传入的变量时,使用指针就可以解决

func main() {
	var a float64 = 3.14
	v := reflect.ValueOf(&a)
	ret := v.CanSet()
	print(ret)  // false
}

虽然传入了指针,但是还是不可以设置,v还是不代表a,v.Elem()才代表a, 这样就可以真正操作a了

func main() {
	var a float64 = 3.14
	v := reflect.ValueOf(&a).Elem()
	ret := v.CanSet()
	fmt.Println(ret)  // true

	v.SetFloat(8.88)
	fmt.Println(a)  // 8.88
}
  • 关于第3条,记住一句话,如果想要操作原变量,反射变量Value必须能hold住原变量的地址才行

什么情况下需要使用反射

  • 使用反射的常见场景有一下两种
  1. 不能明确接口调用哪个函数,需要根据传入的参数在运行时决定
  2. 不能明确传入函数的参数类型,需要在运行时处理任意对象

【引申1】不推荐使用反射的理由有哪些?

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

什么是反射?

  1. 在计算机科学中,反射是指计算机程序在运行时(Run time)可以访问、检测、修改它本身状态和行为的一种能力,
    用比喻来说,反射就是程序在运行时能够观察并修改自己的行为
  2. 难道不用反射就不能在运行时(Run time)访问、检测、修改自身的状态和行为吗?
  3. 问题的回答,其实首先要理解什么叫访问、检测、修改自身的状态和行为,它的 本质是什么
  4. 实际上它的本质是程序在运行期间探知对象的类型信息和内存结构,不用反射能行吗?可以的,使用汇编语言,直接和内存打交道
    可以获取任何信息,当迁到高级语言上来就不行了,只能通过反射来达到此项技能,
  5. 不同语言的反射模型不尽相同,有些语言还不支持反射,go语言圣经中这样定义的反射
    go语言提供了一种机制在运行时Run time时更新变量和检查它们的值,调用它们的方法,但是在编译时并不知道变量的具体类型
    这称为反射机制

如何比较两个对象完全相同

  1. go中提供了一个函数可以完成此项功能,DeepEqual()
    func DeepEqual(x, y interface{}) bool {
    注意,如果x和y的静态类型不一样的话,即使底层类型相同,值也相同,x和y也是不相等的
type MyInt int
type YourInt int

func main() {
	var x = MyInt(1)
	var y = YourInt(1)
	fmt.Println(reflect.DeepEqual(x, y))  // false
}

上面代码中,即使x和y的底层类型都是int, 值都是1,但是两者的静态类型不一致,所以也不是深度相等
类型 深度相等情形
Array 相同索引处的元素“深度”相等
Struct 相应字段,包含导出和不导出,“深度”相等
Func 只有两者都是 nil 时
Interface 两者存储的具体值“深度”相等
Map 1、都为 nil;2、非空、长度相等,指向同一个 map 实体对象,或者相应的 key 指向的 value “深度”相等
Pointer 1、使用 == 比较的结果相等;2、指向的实体“深度”相等
Slice 1、都为 nil;2、非空、长度相等,首元素指向同一个底层数组的相同元素,即 &x[0] == &y[0] 或者 相同索引处的元素“深度”相等
numbers, bools, strings, and channels 使用 == 比较的结果为真

一般情况下,DeepEqual 的实现只需要递归地调用 == 就可以比较两个变量是否是真的“深度”相等。

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

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

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

对于“有环”的类型,比如循环链表,比较两者是否“深度”相等的过程中,需要对已比较的内容作一个标记,一旦发现两个指针之前比较过,立即停止比较,并判定二者是深度相等的。这样做的原因是,及时停止比较,避免陷入无限循环。

posted @ 2022-03-16 11:05  专职  阅读(81)  评论(0编辑  收藏  举报