golang基础之反射

354 阅读13分钟

1 反射是什么

我们在编写代码时,很清晰的知道程序中定义的变量(golang中,变量是存储值的地方)的变量名是什么,数据类型是什么,如果数据类型是一个结构体(struct),也清晰的知道结构体有哪些成员字段(Field),即使是一个接口(interface)变量,我们也可以通过分析程序的执行流程确定接口的动态类型(或者说具体类型,Concrete Type)。但是经过编译器、连接器、加载器处理后运行程序时,程序执行者(CPU)看到的是地址(确定到内存的什么位置获取指令和数据)和值(内存指定地址处存储的二进制比特序列),然后执行指令规定的动作。也就是说,我们在编程时所定义的变量名啊,数据类型什么的是面向编译器和编程人员的,是帮助大家理解的标记符,他们不是面向CPU的。在程序运行时(运行态),CPU并不知道执行的指令和数据相应的变量名和数据类型分别是什么。golang语言的反射(reflect)功能就是一种让程序在运行时具备获取值的数据类型、名字、布局(比如结构体有哪些成员,这些成员的值和类型等信息)、方法等信息并修改值的内容的能力。编译是将变量名、数据类型映射到地址和值,反射是将地址和值映射到编程时定义的变量名和数据类型,与编译的过程相反。

反射功能极大地增强了编程语言的表现能力,尤其在面向输入数据非常灵活,无法事先确定具体的类型和值时,通过反射可以非常灵活的统一处理不同的数据类型和值,使代码更加紧凑和具备更广泛的适应性,不需要根据每一种情况编写相应的代码。比较典型和常用的例子是json字符串的反序列化,输入的json字符串是千变万化的,输出的对象类型也是无法穷举的,使用反射功能就可以轻松的将json字符串与用户指定的类型对象关联起来。

在编程语言中,与反射相近的一个概念是内省(inspect)。一般来说,內省只能查看值的数据类型、布局等信息,但不能修改值的内容。但是反射既能查看也能修改。

2 反射的基本操作

golang在reflect包中定义了两个接口:TypeValue,通过它们来统一表示类型和值。

2.1 Type

golang在reflect包中定义了很多实现了Type接口的结构体,他们分别存储了不同的数据类型的Type信息,但是这些Type接口实现都是无法包外访问的,所以我们无法直接创建这些类型的变量。同时,reflect包提供了一个方法TypeOf()来获取任意对象的Type。TypeOf()方法的定义为:

func TypeOf(i any) Type

这样,我们可以通过以下方法获取变量的Type:

vInt := 100
typeOfInt := reflect.TypeOf(vInt)

获取了变量的Type后,我们就可以通过Type接口的Name()和String()方法获取类型的名字和格式化字符串信息,如下:

fmt.Printf("typeOfInt name: %s, formatted string: %s\n", typeOfInt.Name(), typeOfInt.String())

结果输出正是数据类型的名字:

typeOfInt name: int, formatted string: int

我们再看看一些常见数据类型的Name()和String()方法输出。

type Enum int

type Person struct {
   Name string
}

func main() {

   var vUint uint = 123
   typeOfUint := reflect.TypeOf(vUint)
   fmt.Printf("typeOfUint name: %s, formatted string: %s\n", typeOfUint.Name(), typeOfUint.String())

   vStr := "123"
   typeOfString := reflect.TypeOf(vStr)
   fmt.Printf("typeOfString name: %s, formatted string: %s\n", typeOfString.Name(), typeOfString.String())

   var vEnum Enum = 100
   typeOfEnum := reflect.TypeOf(vEnum)
   fmt.Printf("typeOfEnum name: %s, formatted string: %s\n", typeOfEnum.Name(), typeOfEnum.String())

   vStruct :=  Person{Name: "Jon"}
   typeOfStruct := reflect.TypeOf(vStruct)
   fmt.Printf("typeOfStruct name: %s, formatted string: %s\n", typeOfStruct.Name(), typeOfStruct.String())

   vArray := [3]int{}
   typeOfArray := reflect.TypeOf(vArray)
   fmt.Printf("typeOfArray name: %s, formatted string: %s\n", typeOfArray.Name(), typeOfArray.String())

   vSlice := []int{}
   typeOfSlice := reflect.TypeOf(vSlice)
   fmt.Printf("typeOfSlice name: %s, formatted string: %s\n", typeOfSlice.Name(), typeOfSlice.String())

   vMap := map[string]int{}
   typeOfMap := reflect.TypeOf(vMap)
   fmt.Printf("typeOfMap name: %s, formatted string: %s\n", typeOfMap.Name(), typeOfMap.String())

   vPtr := &vUint
   typeOfPtr := reflect.TypeOf(vPtr)
   fmt.Printf("typeOfPtr name: %s, formatted string: %s\n", typeOfPtr.Name(), typeOfPtr.String())

}

输出:

typeOfUint name: uint, formatted string: uint
typeOfString name: string, formatted string: string
typeOfEnum name: Enum, formatted string: main.Enum
typeOfStruct name: Person, formatted string: main.Person
typeOfArray name: , formatted string: [3]int
typeOfSlice name: , formatted string: []int
typeOfMap name: , formatted string: map[string]int
typeOfPtr name: , formatted string: *uint

从输出结果可以看出,引用类型的Name值为空,自定义数据类型的格式化字符串包含了包名字

再来看看接口的例子

type Work interface {
   DoWork()
}

type Engineer struct {
   Name string
}

func (e Engineer) DoWork() {
   fmt.Printf("Hello, %v", e.Name)
}

type Artist struct {
   Name string
}

func (e *Artist) DoWork() {
   fmt.Printf("Hello, %v", e.Name)
}


func main() {

   var vInterface Work

   vInterface = Engineer{}
   typeOfInterface := reflect.TypeOf(vInterface)
   fmt.Printf("typeOfInterface name: %s, formatted string: %s\n", typeOfInterface.Name(), typeOfInterface.String())

   vInterface = &Artist{}
   typeOfPtrInterface := reflect.TypeOf(vInterface)
   fmt.Printf("typeOfPtrInterface name: %s, formatted string: %s\n", typeOfPtrInterface.Name(), typeOfPtrInterface.String())
}

输出:

typeOfInterface name: Engineer, formatted string: main.Engineer
typeOfPtrInterface name: , formatted string: *main.Artist

可见接口的Type是接口的动态类型,而不是接口类型本身(静态类型)。事实上,接口的Type永远是接口的具体实现类型

因为Type接口实现了fmt.Stringer接口,所以格式化字符串时可以用%T占位符得到变量的类型,如下所示:

vInt := 2
fmt.Printf("formatted string: %T, %T\n", vInt, &vInt)

输出:

formatted string: int, *int

2.2 Value

与Type接口类似,reflect包提供了ValueOf(i any)函数获取任意变量的Value,格式化字符串时使用%v占位符打印变量值的内容,同时Value也实现了fmt.Stringer接口。举个简单的例子:

vInt := 2
valueOfInt := reflect.ValueOf(vInt)
fmt.Printf("formatted string: %s, %v, stringer: %s\n", valueOfInt, valueOfInt, valueOfInt.String())

输出:

formatted string: %!s(int=2), 2, stringer: <int Value>

2.3 Type、Value、Interface相互转换

如上所述,我们可以通过reflect.TypeOf()、reflect.ValueOf()方法获取interface的Type和Value。同时我们可以通过Value的Type()方法得到Value的Type,通过Value的Interface()方法得到interface值

vInt := 2
valueOfInt := reflect.ValueOf(vInt)
typeOfInt := valueOfInt.Type()
fmt.Printf("typeOfInt: %s\n", typeOfInt.String())

vInterface := valueOfInt.Interface()
x := vInterface.(int) + 1
fmt.Printf("x value: %d\n", x)

结果输出:

typeOfInt: int
x value: 3

其实,Value和interface都包含了对象的类型和值信息,但是Value接口提供了更加丰富的方法来查看和操作对象的类型和值,后面会详细介绍这些接口。但是Type只包含了类型信息,所以没有从Type获取Value和interface值的方法

2.4 Type和Value的Kind

因为golang支持自定义数据类型,所以应用程序的数据类型可以是无数的,也就是说Type代表的数据类型是无数的。好消息是,我们可以把系统所有的数据类型归类为少量的固定的几个类型(kind),并可以通过Type或Value的Kind()方法获取相应数据类型所属的kind。系统所有的数据类型可以归属为以下的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
	Pointer
	Slice
	String
	Struct
	UnsafePointer
)

知道Type或Value所属的kind非常有用,我们通过根据Type或Value的kind值做相应的处理,如下所示:

vInt := 2
valueOfInt := reflect.ValueOf(vInt)
kindOfInt := valueOfInt.Kind()

switch kindOfInt {
case reflect.Int:
	fmt.Printf("%d", valueOfInt.Interface().(int))
case reflect.String:
	// do something
// case other kind
}

在Kind的枚举列表中,我们还看到了叫做Interface的枚举值。但是reflect.TypeOf()和reflect.ValueOf()方法获取的都永远是interface的具体类型,为什么还会出现interface类型呢?答案在下面揭晓。

3 Type的常用操作

上面我们介绍了Type接口的Name()、String()、Kind()三个方法,下面介绍Type其他的一些常用方法,Type的能量更多的体现在下面介绍的方法。首先把当前Type的方法都列举出来,如下所示:

type Type interface {
	Align() int
	FieldAlign() int

	Name() string
	// 包路径
	PkgPath() string
	// 数据类型值占用的字节数
	Size() uintptr
	String() string
	Kind() Kind
	
	Implements(u Type) bool
	AssignableTo(u Type) bool
	ConvertibleTo(u Type) bool
	Comparable() bool
	
	Len() int
	Bits() int
	ChanDir() ChanDir
	IsVariadic() bool
	
	// 元素类型
	Key() Type
	Elem() Type
	
	// 成员字段
	NumField() int
	Field(i int) StructField
	FieldByIndex(index []int) StructField
	FieldByName(name string) (StructField, bool)
	FieldByNameFunc(match func(string) bool) (StructField, bool)
	
	// 方法
	Method(int) Method
	MethodByName(string) (Method, bool)
	NumMethod() int
	
	// 函数
	In(i int) Type
	NumIn() int
	NumOut() int
	Out(i int) Type

	common() *rtype
	uncommon() *uncommonType
}

3.1 Len()

对于数组(Array)类型,可以通过Len()方法获取数组的大小

3.2 Key()、Elem()

对于数组(Array)、切片(Slice)、指针(Pointer)、map、通道(channel)等聚合或引用类型,可以通过Elem()方法获取对应元素的类型,比如,*string、[]string、map[int]string类型的Elem()方法返回的都是string类型。对于map类型,还可以通过Key()方法获取map key的数据类型,比如map[int]string类型的Key()方法返回的是int类型

同时,Elem()方法获取的是元素的静态类型,如果元素是接口(interface)类型,那么Elem()方法返回的Type是interface,相应的Kind值也是interface。举一个常见的例子:

type Work interface {
	DoWork()
	doMoreWork()
}



func (e Engineer) DoWork() {
	fmt.Printf("Hello, %v", e.Name)
}

func (e Engineer) doMoreWork() {
	fmt.Printf("Hello, %v", e.Name)
}

func main() {

	// 空接口
	var vEmptyInterface interface{} = 1
	elemType := reflect.TypeOf(&vEmptyInterface).Elem()
	fmt.Printf("vEmptyInterface element stringer: %s, Kind: %s\n",elemType.String(),  elemType.Kind())

	// 空接口
	var vSlice = []interface{}{1, "2"}
	elemType = reflect.TypeOf(vSlice).Elem()
	fmt.Printf("vSlice element stringer: %s, Kind: %s\n",elemType.String(),  elemType.Kind())

	// 接口
	var vInterface Work = Engineer{"Engineer"}
	elemType = reflect.TypeOf(&vInterface).Elem()
	fmt.Printf("vInterface element stringer: %s, Kind: %s\n",elemType.String(),  elemType.Kind())
}

输出:

vEmptyInterface element stringer: interface {}, Kind: interface
vSlice element stringer: interface {}, Kind: interface
vInterface element stringer: main.Work, Kind: interface

3.3 NumField()、Field()、FieldByIndex()、FieldByName()、FieldByNameFunc()

对于结构体(struct)类型,可以通过NumField()获取结构的成员字段的个数,通过Field()、FieldByIndex()、FieldByName()、FieldByNameFunc()方法获取指定成员字段的字段类型。举个例子:

type Engineer struct {
	Name string `json:"name"`
}

func main() {

	var x Engineer
	x.Name = "John"

	tp := reflect.TypeOf(x)
	// 字段的数量
	fmt.Printf("field number of Engineer: %d\n", tp.NumField())

	fieldType := tp.Field(0)
	// name字段的名字
	fmt.Printf("field name of Engineer: %s\n", fieldType.Name)
}

输出为:

field number of Engineer: 1
field name of Engineer: Name

其实,Type的Field()系列方法返回的不是一个Type接口,而是一个StructField接口,它的定义为:

// A StructField describes a single field in a struct.
type StructField struct {
	// Name is the field name.
	Name string

	// PkgPath is the package path that qualifies a lower case (unexported)
	// field name. It is empty for upper case (exported) field names.
	// See https://golang.org/ref/spec#Uniqueness_of_identifiers
	PkgPath string

	Type      Type      // field type
	Tag       StructTag // field tag string
	Offset    uintptr   // offset within struct, in bytes
	Index     []int     // index sequence for Type.FieldByIndex
	Anonymous bool      // is an embedded field
}

可见,StructField接口的一个重要功能是可以获取字段的Tag信息,StructTag接口。并可以通过StructTag接口的Get()、Lookup()方法获取指定Tag的值。接上面的例子,查看Engineer结构体Name字段的Tag:

// 字段添加了json tag,没有添加xml tag
jsonTagValue := fieldType.Tag.Get("json")
xmlTagValue, xmlTagExisted := fieldType.Tag.Lookup("xml")
fmt.Printf("jsonTagValue: %s, xmlTagExisted: %t, xmlTagValue: %s\n", jsonTagValue, xmlTagExisted, xmlTagValue)

上面代码输出:

jsonTagValue: name, xmlTagExisted: false, xmlTagValue: 

3.4 NumMethod()、Method()、MethodByName()

对于非接口类型(典型的如结构体),NumMethod()返回的是导出方法的个数。对于接口类型,返回的是导出和非导出接口的中个数。Method()和MethodByName()方法则返回类型的指定方法。Method()和MethodByName()返回的是Method接口,Method接口的定义如下:

// Method represents a single method.
type Method struct {
	// Name is the method name.
	Name string

	// PkgPath is the package path that qualifies a lower case (unexported)
	// method name. It is empty for upper case (exported) method names.
	// The combination of PkgPath and Name uniquely identifies a method
	// in a method set.
	// See https://golang.org/ref/spec#Uniqueness_of_identifiers
	PkgPath string

	Type  Type  // method type
	Func  Value // func with receiver as first argument
	Index int   // index for Type.Method
}

3.5 NumIn()、In()、NumOut()、Out()

如果类型是函数,分别可以通过NumIn()、In()、NumOut()、Out()获取函数的输入参数个数、指定输入参数类型、输出参数个数、指定输出参数类型

4 Value的常用操作

Value与Type类似,除了上面提到的String()、Type()、Interface()、Kind()提供了很多方法查看和操作变量的值。

4.1 IsValid()、IsZero()、IsNil()

IsValid()方法可以判断Value是否有效,只有有效的Value,才能调用Value的相应方法,否则会panic。一般值的Value都是有效的,但是没有指定具体类型的接口是invalid的。例子:

var work Work
v = reflect.ValueOf(work)
fmt.Printf("%t\n", v.IsValid())  // 输出为false

v.isZero() // 会panic

IsZero()用来判断Value是不是相应数据类型的零值,例如:

var vPtr *int
v := reflect.ValueOf(vPtr)
fmt.Printf("%t\n", v.IsZero()) // 输出为true

var vInt int
v = reflect.ValueOf(vInt)
fmt.Printf("%t\n", v.IsZero()) // 输出为true

var vChan []int
v = reflect.ValueOf(vChan)
fmt.Printf("%t\n", v.IsZero()) // 输出为true

当数据类型是chan、func、interface、map、pointer或slice这些可以取nil值的类型时,可以用IsNil()方法来判断Value是不是nil值。例如:

var vPtr *int
v := reflect.ValueOf(vPtr)
fmt.Printf("%t\n", v.IsNil()) // 输出为true

4.2 Can()系列方法

Value提供了一系列方法来检测Value是否具备某项能力的方法。只有Value具备相应的能力时,才能执行相应的功能。如CanInt()返回true,才能调用Int()方法提取Value值为int,否则会panic。只有CanAddr()、CanSet()方法返回结果为true,才能设置Value的值。

我们通常需要使用Value来设置变量的值,所以Value是否是可寻址的(addressable)很重要。那么哪些value是可寻址的呢?一般来说,数组的元素、slice的元素、指针的解引用、可寻址的结构体的所有成员字段。可寻址的结构体的所有成员字段是可寻址的,这是一个很重要的结论,意味着只要一个结构体是可以寻址,那么不管它的成员是什么数据类型,也不管是递归了多少层的孙子成员,都是可寻址的,都是可以修改的。举几个例子:

var vInt int
fmt.Printf("%t\n", reflect.ValueOf(vInt).CanAddr()) // 输出false
fmt.Printf("%t\n", reflect.ValueOf(&vInt).CanAddr()) // 输出false
fmt.Printf("%t\n", reflect.ValueOf(&vInt).Elem().CanAddr()) // 输出true

从上面的例子可以看出,vInt、vInt的指针都是不可寻址的。但是vInt的指针指向的值是可以寻址的。这是因为golang是值传递,reflect.ValueOf(vInt)、reflect.ValueOf(&vInt)的Value存储的仅是变量的值,变量的指针值,但是通过Elem()解引用后的Value存储的变量存储空间位置信息,所以是可寻址的。仔细理解这一点,对正确使用Value来说非常关键

4.3 查看Value的值

如果Value对应的数据类型是基本数据类型,可以通过Int()、UInt()、Float()等方法获取相应的值

如果Value是指针、接口,可以通过Elem()方法分别获取指针指向的变量,接口的具体值

如果Value是数组、切片、map、字符串、通道或数组的指针,可以通过Len()方法获取元素的长度;如果是数组、切片、通道或数组的指针,还可以通过Cap()方法获取容量值

如果Value是数组、切片、字符串,可以通过Index()获取相应元素的Value

如果Value是map,可以通过MapKeys()、MapIndex()分别获取map的key-value映射的Value

如果Value是结构体,可以通过NumberField()获取结构体成员的个数,Field()、FieldByIndex()、FieldByName()获取指定的成员

我们可以通过NumberMethod()方法获取Value的方法数量,Method()、MethodByName()获取Value的指定方法

4.4 创建Value

reflect包提供了一系列方法创建Value,New()、MakeSlice()、MakeMap()、MakeChan()等方法用法与基本golang编程语法类型,只是参数是Type,结果也是用Value表示的

4.5 修改Value

如果Value是可寻址(CanAddr()返回true)、可以设置的(CanSet()返回true),我们可以使用Set()方法设置Value的值。如果是int、bool、string这些基本类型,还可以通过SetInt()、SetBool()、SetString()等相应的特定方法进行设置

如果是通道类型,可以通过Send()、Recv()等方法进行发送和接收消息

4.6 调用函数或方法

可以通过Call()方法调用函数,举例如下:

func add(a, b int) int {
	return a + b
}

func main() {

	funcValue := reflect.ValueOf(add)
	// 函数参数
	args := []reflect.Value{reflect.ValueOf(10), reflect.ValueOf(20)}
	// 调用函数
	results := funcValue.Call(args)
	// 获取结果
	fmt.Println(results[0].Int()) // 输出30

}

调用方法的例子:

type Foo int
func (f Foo) Add(a, b int) int {
	return a + b
}

func main() {

	var foo Foo = 1
	v := reflect.ValueOf(foo)
	// 获取方法Value
	funcValue := v.MethodByName("Add")

	// 函数参数
	args := []reflect.Value{reflect.ValueOf(1), reflect.ValueOf(2)}
	// 调用函数
	results := funcValue.Call(args)
	// 获取结果
	fmt.Println(results[0].Int()) // 输出3


}