这是我参与「第五届青训营 」伴学笔记创作活动的第 7 天
GO reflect 机制
Created: January 30, 2023 10:34 PM Tags: golang note
Go 语言的反射特性(reflect)提供了运行时动态获取对象的类型和值以及动态创建对象的能力,这使得 Go 虽然是一门强类型的静态编程语言,却具备一些动态语言的特性,提高我们的开发效率。
反射是一种机制,使用反射可以让我们编写出能统一处理所有类型的代码,一个具体的例子就是 fmt.Println() 方法,可以打印出我们自定义的结构类型。虽然反射可以帮助抽象和简化代码,提高开发效率,但反射也存在十分明显的缺点,例如影响性能、不易阅读、以及把编程时就能检查出来的类型问题推迟到运行时以 panic 形式表现出来,这也是一般不建议在代码中使用反射的原因。但是如果能理解反射的机制原理,能够帮助我们深入理解许多标准库中的源码,例如 encoding/json,encoding/xml 等。
reflect 实现
Go 语言的反射是建立在 Go 的类型系统之上的,并且与接口密切相关,关于接口的内容之前写过另一篇文章,在此就不再多做介绍。
Go 语言中的反射功能由reflect包提供。reflect包定义了一个接口reflect.Type和一个结构体reflect.Value,它们定义了大量的方法用于获取类型信息,设置值等。在reflect包内部,只有类型描述符实现了reflect.Type接口。由于类型描述符是未导出类型,我们只能通过reflect.TypeOf()方法获取reflect.Type类型的值:
package main
import (
"fmt"
"reflect"
)
type Cat struct {
Name string
}
func main() {
var f float64 = 3.5
t1 := reflect.TypeOf(f)
fmt.Println(t1.String())
c := Cat{Name: "kitty"}
t2 := reflect.TypeOf(c)
fmt.Println(t2.String())
}
输出:
float64
main.Cat
Go 语言是静态类型的,每个变量在编译期有且只能有一个确定的、已知的类型,即变量的静态类型。静态类型在变量声明的时候就已经确定了,无法修改。一个接口变量,它的静态类型就是该接口类型。虽然在运行时可以将不同类型的值赋值给它,改变的也只是它内部的动态类型和动态值。它的静态类型始终没有改变。reflect.TypeOf()方法就是用来取出接口中的动态类型部分,以reflect.Type返回。我们看下reflect.TypeOf()的定义:
// src/reflect/type.go
func TypeOf(i interface{}) Type {
eface := *(*emptyInterface)(unsafe.Pointer(&i))
return toType(eface.typ)
}
它接受一个interface{}类型的参数,所以上面的float64和Cat变量会先转为interface{}再传给方法,reflect.TypeOf()方法获取的就是这个interface{}中的类型部分。
相应地,reflect.ValueOf()方法自然就是获取接口中的值部分,返回值为reflect.Value类型。在上例基础上添加下面代码:
v1 := reflect.ValueOf(f)
fmt.Println(v1)
fmt.Println(v1.String())
v2 := reflect.ValueOf(c)
fmt.Println(v2)
fmt.Println(v2.String())
运行输出:
3.5
<float64 Value>
{kitty}
<main.Cat Value>
由于fmt.Println()会对reflect.Value类型做特殊处理,打印其内部的值,所以上面显示调用了reflect.Value.String()方法获取更多信息。
获取类型如此常见,fmt提供了格式化符号%T输出参数类型:
fmt.Printf("%T\n", 3) // int
Go 语言中类型是无限的,而且可以通过type定义新的类型。但是类型的种类是有限的,reflect包中定义了所有种类的枚举:
// src/reflect/type.go
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
)
Go 中所有的类型(包括自定义的类型),都是上面这些类型或它们的组合。
reflect 使用
由于反射的内容和 API 非常多,我们结合具体用法来介绍。
透视数据组成
透视结构体组成,需要以下方法:
reflect.ValueOf():获取反射值对象;reflect.Value.NumField():从结构体的反射值对象中获取它的字段个数;reflect.Value.Field(i):从结构体的反射值对象中获取第i个字段的反射值对象;reflect.Kind():从反射值对象中获取种类;reflect.Int()/reflect.Uint()/reflect.String()/reflect.Bool():这些方法从反射值对象做取出具体类型。
示例:
type User struct {
NamestringAgeintMarriedbool}
func inspectStruct(u interface{}) {
v := reflect.ValueOf(u)
for i :=0; i < v.NumField(); i++ {
field := v.Field(i)
switch field.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
fmt.Printf("field:%d type:%s value:%d\n", i, field.Type().Name(), field.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
fmt.Printf("field:%d type:%s value:%d\n", i, field.Type().Name(), field.Uint())
case reflect.Bool:
fmt.Printf("field:%d type:%s value:%t\n", i, field.Type().Name(), field.Bool())
case reflect.String:
fmt.Printf("field:%d type:%s value:%q\n", i, field.Type().Name(), field.String())
default:
fmt.Printf("field:%d unhandled kind:%s\n", i, field.Kind())
}
}
}
func main() {
u := User{
Name: "dj",
Age:18,
Married:true,
}
inspectStruct(u)
}
结合使用reflect.Value的NumField()和Field()方法可以遍历结构体的每个字段。然后针对每个字段的Kind做相应的处理。
有些方法只有在原对象是某种特定类型时,才能调用。例如NumField()和Field()方法只有原对象是结构体时才能调用,否则会panic。
识别出具体类型后,可以调用反射值对象的对应类型方法获取具体类型的值,例如上面的field.Int()/field.Uint()/field.Bool()/field.String()。
透视map组成,需要以下方法:
reflect.Value.MapKeys():将每个键的reflect.Value对象组成一个切片返回;reflect.Value.MapIndex(k):传入键的reflect.Value对象,返回值的reflect.Value;- 然后可以对键和值的
reflect.Value进行和上面一样的处理。
透视切片或数组组成,需要以下方法:
reflect.Value.Len():返回数组或切片的长度;reflect.Value.Index(i):返回第i个元素的reflect.Value值;- 然后对这个
reflect.Value判断Kind()进行处理。
透视函数类型,需要以下方法:
reflect.Type.NumIn():获取函数参数个数;reflect.Type.In(i):获取第i个参数的reflect.Type;reflect.Type.NumOut():获取函数返回值个数;reflect.Type.Out(i):获取第i个返回值的reflect.Type。
透视结构体中定义的方法,需要以下方法:
reflect.Type.NumMethod():返回结构体定义的方法个数;reflect.Type.Method(i):返回第i个方法的reflect.Method对象;
调用函数或方法
调用函数,需要以下方法:
reflect.Value.Call():使用reflect.ValueOf()生成每个参数的反射值对象,然后组成切片传给Call()方法。Call()方法执行函数调用,返回[]reflect.Value。其中每个元素都是原返回值的反射值对象。
示例:
func Add(a, bint)int {
return a + b
}
func Greeting(namestring)string {
return "hello " + name
}
func invoke(f interface{}, args ...interface{}) {
v := reflect.ValueOf(f)
argsV := make([]reflect.Value,0, len(args))
for _, arg := range args {
argsV = append(argsV, reflect.ValueOf(arg))
}
rets := v.Call(argsV)
fmt.Println("ret:")
for _, ret := range rets {
fmt.Println(ret.Interface())
}
}
func main() {
invoke(Add,1,2)
invoke(Greeting, "dj")
}
我们封装一个invoke()方法,以interface{}空接口接收函数对象,以interface{}可变参数接收函数调用的参数。函数内部首先调用reflect.ValueOf()方法获得函数对象的反射值对象。然后依次对每个参数调用reflect.ValueOf(),生成参数的反射值对象切片。最后调用函数反射值对象的Call()方法,输出返回值。
程序运行结果:
ret:
3
ret:
hello dj
以上是在编译期明确知道方法名的情况下发起调用。如果只给一个结构体对象,通过参数指定具体调用哪个方法该怎么做呢?这需要以下方法:
reflect.Value.MethodByName(name):获取结构体中定义的名为name的方法的reflect.Value对象,这个方法默认有接收器参数,即调用MethodByName()方法的reflect.Value。
示例:
type Math struct {
a, bint}
func (m Math) Add()int {
return m.a + m.b
}
func (m Math) Sub()int {
return m.a - m.b
}
func (m Math) Mul()int {
return m.a * m.b
}
func (m Math) Div()int {
return m.a / m.b
}
func invokeMethod(obj interface{}, namestring, args ...interface{}) {
v := reflect.ValueOf(obj)
m := v.MethodByName(name)
argsV := make([]reflect.Value,0, len(args))
for _, arg := range args {
argsV = append(argsV, reflect.ValueOf(arg))
}
rets := m.Call(argsV)
fmt.Println("ret:")
for _, ret := range rets {
fmt.Println(ret.Interface())
}
}
func main() {
m := Math{a:10, b:2}
invokeMethod(m, "Add")
invokeMethod(m, "Sub")
invokeMethod(m, "Mul")
invokeMethod(m, "Div")
}
我们可以在结构体的反射值对象上使用NumMethod()和Method()遍历它定义的所有方法。