细说Golang的反射

一、什么是反射?

Go 语言提供了一种机制在运行时更新变量和检查它们的值、调用它们的方法,但是在编译时并不知道这些变量的具体类型,这称为反射机制。

二、为什么要用反射?

以下是需要反射的 2 个常见场景:

  1. 有时你需要编写一个函数,但是并不知道传给你的参数类型是什么,可能是没约定好;也可能是传入的类型很多,这些类型并不能统一表示。这时反射就会用的上了
  2. 有时候需要根据某些条件决定调用哪个函数,比如根据用户的输入来决定。这时就需要对函数和函数的参数进行反射,在运行期间动态地执行函数。

三、反射是如何实现的?

我们以前学习过interface,当向接口变量赋予一个实体类型的时候,接口会存储实体的类型信息,反射就是通过接口的类型信息实现的,反射建立在类型的基础上。而Go语言在 reflect 包里定义了各种类型,实现了反射的各种函数,通过它们可以在运行时检测类型的信息、改变类型的值。

1、相关概念

Go语言的变量类型

变量包括(type, value)两部分,而type 包括 static type和concrete type。static type是你在编码是看见的类型(如int、string),concrete type是runtime系统看见的类型。

静态类型&动态类型

Go 语言中,每个变量都有一个静态类型,在编译阶段就确定了,而运行时才知道变量类型的叫做动态类型

示例:

type MyInt int

var i int
var j MyInt

我们都知道i和j的底层类型都是int,但是他们是不同的静态类型,i和j不能比较大小等数值比较的操作,因为j 的静态类型是 MyInt,不然会编译报错mismatched types int and MyInt

  • Golang的指定类型的变量的类型是静态的,在创建变量的时候就已经确定了。
  • 反射主要与Golang的interface类型相关,只有interface类型才有反射一说
var A interface{} // 静态类型interface{}
A = 10            // 静态类型为interface{}  动态为int
A = "String"      // 静态类型为interface{}  动态为string
var M *int
A = M             // A的值可以改变
接口变量的pair

在Go的实现中,每个interface变量都有一个对应pair,pair中记录了实际变量的值和类型:(value, type),value是实际变量值,type是实际变量的类型。一个interface{}类型的变量包含了2个指针,一个指针指向值的类型(concrete type),另外一个指针指向实际的值(value)。

  • interface及其pair的存在,是Go实现反射的前提
  • 反射就是用来检测存储在接口变量内部pair对的一种机制。

2、reflect包-实现反射

在 Go 语言中,reflect 实现了运行时反射。reflect 包会帮助识别 interface{} 变量的底层具体类型和具体值。

接下来,我们来了解reflect 包中的几种类型和方法:

①reflect.Type 和 reflect.Value

reflect.Type 表示 interface{} 的具体类型,而 reflect.Value 表示它的具体值。
reflect.TypeOf() 和 reflect.ValueOf() 两个函数可以分别返回 reflect.Type 和 reflect.Value。

//ValueOf用来获取输入参数接口中的数据的值,如果接口为空(nil)则返回0
func ValueOf(i interface{}) Value {...}
//TypeOf用来动态获取输入参数接口中的值的类型,如果接口为空(nil)则返回nil
func TypeOf(i interface{}) Type {...}

示例:

package main

import (
	"fmt"
	"reflect"
)

type myInt int

type st struct {
	name string
	number int
}

type st2 struct {
	name string
	number int
}

func check(i interface{})  {
	ty := reflect.TypeOf(i)
	value := reflect.ValueOf(i)
	fmt.Println("Type ", ty)
	fmt.Println("Value ", value)
}

func main() {
	var v1 myInt
	v1 = 1
	v2 := st{name: "st1", number: 1}
	v3 := st2{name: "st2", number: 2}
	fmt.Println("====v1======")
	check(v1)
	fmt.Println("====v2======")
	check(v2)
	fmt.Println("====v3======")
	check(v3)
}

输出:

====v1======
Type  main.myInt
Value  1
====v2======
Type  main.st
Value  {st1 1}
====v3======
Type  main.st2
Value  {st2 2}
②reflect.Kind

reflect.Type 变量 和 reflect.Value 变量都可以通过 Kind() 方法返回对应的接口变量的基础类型。

reflect/type.go

type Type interface {
...
Kind() Kind
...
}

reflect/value.go

func (v Value) Kind() Kind{
	...
}
  • Kind()方法返回的Kind和reflect.TypeOf返回的Type的类型可能看起来很相似,但实际上 Kind 表示的是 Go 原生的基本类型

我们可以阅读源码reflect/type.go 看到Kind的定义:

type Kind uint

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
)

现在我们把上面示例中的check方法增加多两句输出,也可以看到Kind方法实际返回的类型:

...
func check(i interface{})  {
	ty := reflect.TypeOf(i)
	value := reflect.ValueOf(i)
	fmt.Println("Type ", ty)
	fmt.Println("Value ", value)
	fmt.Println("Type.Kind() ", ty.Kind())
	fmt.Println("Value.Kind() ", value.Kind())
}
...

输出:

====v1======
Type  main.myInt
Value  1
Type.Kind()  int
Value.Kind()  int
====v2======
Type  main.st
Value  {st1 1}
Type.Kind()  struct
Value.Kind()  struct
====v3======
Type  main.st2
Value  {st2 2}
Type.Kind()  struct
Value.Kind()  struct
③、NumField() 和 Field()

NumField() 方法返回结构体中字段的数量,而 Field(i int) 方法结构体中第i个字段的reflect.Value。

func (v Value) Field(i int) Value {
	...
} 
...
func (v Value) NumField() int {
	...
}

示例:

package main

import (
	"fmt"
	"reflect"
)

type student struct {
	id int
	name string
	age int
}

func main() {
	stu := student{id: 1001, name: "小黄", age: 16}
	value := reflect.ValueOf(stu)
	fmt.Println("字段数:", value.NumField())

	for i:=0;i<value.NumField();i++{
		fmt.Printf("第 %d 个字段:%v \n", i+1, value.Field(i))
	}
}

输出:

字段数: 3
第 1 个字段:1001 
第 2 个字段:小黄 
第 3 个字段:16 
④、Int() 和 String()
func (v Value) Int() int64 {
	...
}
...
func (v Value) String() string {
	...
}

示例:

package main

import (
	"fmt"
	"reflect"
)

func main() {
	a := 111
	b := "string"
	aValue := reflect.ValueOf(a).Int()
	bValue := reflect.ValueOf(b).String()
	fmt.Println("Int():", aValue)
	fmt.Println("String():", bValue)
}

输出:

Int(): 111
String(): string

四、尽量避免使用反射

几点不太建议使用反射的理由:

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

五、反射的三大定律

Go官方提到的反射三大定律:

  1. Reflection goes from interface value to reflection object.
  2. Reflection goes from reflection object to interface value.
  3. To modify a reflection object, the value must be settable.

翻译:

  1. 反射将接口变量转换成反射对象 Type 和 Value
  2. 通过反射可以将反射对象 Value 还原成原先的接口变量
  3. 若要修改反射对象,该值必须是可设置的
第一条定律

反射将接口变量转换成反射对象 Type 和 Value

这个比较好理解,其实就是通过reflect.TypeOfreflect.ValueOf函数得到接口变量中的具体类型和值。

第二条定律

通过反射可以将反射对象 Value 还原成原先的接口变量

第二条与第一条定律相反,其实就是将reflect.ValueOf方法得到的reflect.Value值通过Interface()方法转回interface 变量。

第三条定律

若要修改反射对象,该值必须是可设置的

这个相对来说比较难理解,我们可以通过一个经典的例子来理解:

package main

import "reflect"

func main() {
	var x int
	x = 1
	v := reflect.ValueOf(x)
	v.SetInt(2) //panic: reflect: reflect.flag.mustBeAssignable using unaddressable value
}

其panic的原因是因为反射变量 v是不可以设置的。为什么v不可以设置,因为传入的参数在ValueOf函数内部只是一个拷贝,是值传递。所以v是不能代表x本身的,对v的修改只是代表了对x的拷贝值进行了修改。

那么如果我们想反射变量是可设置的,应该怎么做?
那就像Go中的引用传递一样,向函数中传入指针变量

...
func main() {
	var x int
	x = 1
	v := reflect.ValueOf(&x)
	fmt.Println(v.CanSet()) //false
}

为什么v还是不可设置?是因为v.Elem() 才真正代表 x

...
func main() {
	var x int
	x = 1
	v := reflect.ValueOf(&x)
	fmt.Println(v.Elem().CanSet()) //true
}

注意:一个可取地址的 reflect.Value 变量会记录一个结构体成员是否是未导出成员,如果是的话则拒绝修改操作。

示例:

package main

import (
	"fmt"
	"reflect"
)

type Person struct {
	Name     string
	isAdult  bool
}

func main() {
	p := Person{Name: "小黄", isAdult: false}

	v := reflect.ValueOf(&p)

	f := v.Elem().FieldByName("Name")

	f.SetString("老黄")
	fmt.Println(f.String())

	f = v.Elem().FieldByName("isAdult")
	f.SetBool(true) //panic: reflect: reflect.flag.mustBeAssignable using value obtained using unexported field
}

你可能感兴趣的:(Golang)