共计 10144 个字符,预计需要花费 26 分钟才能阅读完成。
反射使得 Go 语言具备一些动静个性,比方不晓得参数类型怎么办?当然你能够定义多个函数,别离传递不同参数;你也能够定义一个函数就行,参数类型为 interface{},函数内通过反射操作变量。一些 rpc 框架,通常应用反射注册服务办法,以及通过反射调用服务办法。
反射初体验
如何应用反射呢?咱们以字符串转化函数为例,strconv 包定义了很多函数,能够将 bool 值,int 值,float 值等转化为字符串;然而,如果变量类型不晓得呢?是否封装一个能够转化所有类型到字符串的函数呢?当然能够了,如上一篇文章最初,通过 v.(type) 与 switch 语法,判断变量类型,执行不同转化函数,只是还有一些细节须要非凡解决,如指针类型变量。咱们能够参考 github.com/spf13/cast 依赖库,其实现了不同类型之间的转化函数,转化到字符串的函数如下:
func ToStringE(a interface{}) (string, error) {i = indirectToStringerOrError(i) | |
switch s := i.(type) {// 各类型转化} | |
} | |
func indirectToStringerOrError(a interface{}) interface{} { | |
if a == nil {return nil} | |
// 局部类型实现了 fmt.Stringer 接口(String() string 办法);或者 error 接口(Error() string 办法)// 这些类型只须要调用对用办法转化为字符串就行 | |
var errorType = reflect.TypeOf((*error)(nil)).Elem() | |
var fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem() | |
v := reflect.ValueOf(a) | |
// 指针类型的变量,获取其指向的 value 对象 | |
for !v.Type().Implements(fmtStringerType) && !v.Type().Implements(errorType) && v.Kind() == reflect.Ptr && !v.IsNil() {v = v.Elem() | |
} | |
// 包装为空接口 interface{} | |
return v.Interface()} |
首次看这段代码,可能会不知所云,reflect.TypeOf 是什么不理解,reflect.ValueOf 也看不懂,v.Type().Implements 又有什么作用,等等等等;这些其实都是反射罕用的一些办法以及套路。Go 语言反射规范库定义在包 reflect,最罕用 reflect.Type,示意 Go 语言类型,这是一个接口,定义了很多办法,能够帮忙咱们获取到该类型领有的属性、办法,占用内存大小等等;以及 reflect.Value,示意 Go 语言中的值,其蕴含变量的值,以及该变量的类型信息。
什么类型变量能够转化为字符串呢?int,float,[]byte 等都必定都是能够的;除了这些,局部接口也是能够的,如 fmt.Stringer 接口,如 error 接口,其都定义了办法能够返回该类型字符串形容;另外对于指针类型,想转化为字符串,必定先要获取到其指向的元素类型以及值才行。
type Stringer interface {String() string | |
} | |
type error interface {Error() string | |
} |
上述程序用到的几个函数 / 办法的定义如下:
// TypeOf returns the reflection Type that represents the dynamic type of i. | |
// If i is a nil interface value, TypeOf returns nil. | |
TypeOf(i interface{}) Type | |
// Elem returns a type's element type. | |
// It panics if the type's Kind is not Array, Chan, Map, Pointer, or Slice. | |
// 如果是指针,返回其指向的元素类型 | |
Elem() Type | |
// ValueOf returns a new Value initialized to the concrete value | |
// stored in the interface i. ValueOf(nil) returns the zero Value | |
ValueOf(i interface{}) Value | |
// Implements reports whether the type implements the interface type u | |
Implements(u Type) bool | |
// Kind returns v's Kind | |
(v Value) Kind() Kind | |
// Interface returns v's current value as an interface{}. | |
// 也就是将以后 value 对象包装为空接口 interface{} | |
(v Value) Interface() (i interface{}) |
明确了这些函数 / 办法的含意之后,上述程序应该能够了解了:通过将 nil 强制转化为 error 指针类型形式,获取 error 接口类型;通过将 nil 强制转化为 fmt.Stringer 指针类型形式,获取 fmt.Stringer 接口类型;如果以后变量没有实现 fmt.Stringer 接口,也没有实现 error 接口,并且是指针类型,则获取获取到其指向的元素类型。最初将 value 值包装为空接口类型返回。主函数 ToStringE 再依据变量类型,走不同的字符串转化逻辑。
reflect.Type
reflect.Type,示意 Go 语言类型,这是一个接口,定义了很多办法,能够帮忙咱们获取到该类型领有的属性、办法,占用内存大小等等。上面咱们简略介绍一些罕用函数:
type Type interface { | |
// 办法相干 | |
// NumMethod returns the number of methods accessible using Method | |
NumMethod() int | |
// Method returns the i'th method in the type's method set | |
Method(int) Method | |
// MethodByName returns the method with that name in the type's | |
MethodByName(string) (Method, bool) | |
// 属性字段相干 | |
// NumField returns a struct type's field count. | |
NumField() int | |
// FieldByName returns the struct field with the given name | |
FieldByName(name string) (StructField, bool) | |
// Field returns a struct type's i'th field. | |
Field(i int) StructField | |
// 函数相干 | |
// NumIn returns a function type's input parameter count | |
NumIn() int | |
// NumOut returns a function type's output parameter count. | |
NumOut() int | |
// In returns the type of a function type's i'th input parameter. | |
In(i int) Type | |
// Out returns the type of a function type's i'th output parameter. | |
Out(i int) Type | |
// 其余 | |
// Elem returns a type's element type. | |
// It panics if the type's Kind is not Array, Chan, Map, Pointer, or Slice. | |
Elem() Type | |
// Kind returns the specific kind of this type. | |
Kind() Kind | |
// Size returns the number of bytes needed to store | |
// a value of the given type. | |
Size() uintptr | |
// Implements reports whether the type implements the interface type u. | |
Implements(u Type) bool | |
// AssignableTo reports whether a value of the type is assignable to type u. | |
AssignableTo(u Type) bool | |
} |
这里只列出了局部函数的定义以及正文阐明,还有局部函数没有给出,读者能够查阅 reflect 包。另外留神,很多办法只适宜某些类型,比方函数相干,要求类型必须是 funcType,一旦类型不对,就会抛 panic 异样。
上一篇文章解说构造体时提到,Go 语言所有类型,都有其对应的类型定义。runtime/type.go 文件定义了最根本的类型_type struct;其余类型,如切片类型 slicetype,map 类型 maptype,函数类型 functype,等等都继承自_type。与之对应的,反射 reflect 包也定义了所有类型,如 rtype(与_type 对应),切片类型 sliceType,map 类型 mapType,函数类型 funcType。runtime 包定义的诸多类型其实与 reflect 包定义的诸多类型都是一一对应的。
咱们简略理解下一些罕用类型的定义:
// 构造体,structType + uncommonType + []Method 办法数组,间断存储 | |
// struct { | |
// structType | |
// uncommonType | |
// []Method | |
// } | |
type structType struct { | |
rtype | |
pkgPath name | |
fields []structField // sorted by offset} | |
// 切片类型 | |
type sliceType struct { | |
rtype | |
elem *rtype // slice element type | |
} | |
// 函数类型,funcType + uncommonType + 输入输出参数类型,间断存储 | |
// struct { | |
// funcType | |
// uncommonType | |
// [2]*rtype // [0] is in, [1] is out | |
// } | |
type funcType struct { | |
rtype | |
inCount uint16 | |
outCount uint16 // top bit is set if last input parameter is ... | |
} | |
rtype 是所有类型的父类型,rtype 实现了接口 Type 所有办法,其余类型都继承自 rtype,并且对局部办法进行了重写。咱们以构造体类型为例,试着通过反射拜访构造体定义的属性以及办法:
package main | |
import ( | |
"fmt" | |
"reflect" | |
"strings" | |
) | |
type Human struct { | |
Name string | |
Age int | |
} | |
func (h Human)Eat(food string) error {return nil} | |
func (h Human)Walk(a int) error {return nil} | |
func main() {h := Human{Name: "zhangsan", Age: 20} | |
t := reflect.TypeOf(h) | |
if t.Kind() == reflect.Struct { | |
// 遍历构造体所有字段 | |
for i := 0; i < t.NumField(); i ++ {field := t.Field(i) | |
fmt.Println(fmt.Sprintf("%s %s", field.Name, field.Type.Name())) | |
} | |
// 遍历构造体所有办法 | |
for i := 0; i < t.NumMethod(); i ++ {method := t.Method(i) | |
var in []string | |
var out []string | |
// 打印输出参数类型 | |
for j := 0; j < method.Type.NumIn(); j ++ {in = append(in, method.Type.In(j).Name()) | |
} | |
// 打印输出参数类型 | |
for j := 0; j < method.Type.NumOut(); j ++ {out = append(out, method.Type.Out(j).Name()) | |
} | |
fmt.Println(fmt.Sprintf("%s(%s) %s", method.Name, strings.Join(in, ","), strings.Join(out, ","))) | |
} | |
} | |
} | |
//Name string | |
//Age int | |
//Eat(Human,string) error | |
//Walk(Human,int) error |
看到了吧,通过反射拜访构造体定义的属性以及办法还是比较简单的,其余类型也十分相似,这里就不再赘述。至于底层是如何获取到构造体的各属性以及办法,钻研下下面介绍的 structType 就行了;构造体 structType + uncommonType + []Method 间断存储,structType 构造定义的 []structField 就是所有属性,[]Method 就是所有办法。比方获取构造体任意办法的实现如下;
func (t *rtype) exportedMethods() []method { | |
//t.uncommon 偏移 structType 长度就是 uncommonType | |
ut := t.uncommon() | |
if ut == nil {return nil} | |
return ut.exportedMethods()} | |
func (t *uncommonType) exportedMethods() []method { | |
//xcount 办法数目 | |
if t.xcount == 0 {return nil} | |
//moff 是第一个办法的偏移量;t 偏移 moff,就到了办法数组首地址,再将该段内存转化为 []method | |
return (*[1 << 16]method)(add(unsafe.Pointer(t), uintptr(t.moff), "t.xcount > 0"))[:t.xcount:t.xcount] | |
} |
看到这里,思考下 Implements 办法怎么实现呢?是不是也没那么神秘,其实就是遍历接口类型的所有办法,判断构造体类型是否定义了,如果没有则阐明没有实现该接口。最初,有趣味的读者能够本人钻研下每一种类型的定义,以及反射操作方法,以及这些办法的实现原理。
reflect.Value
reflect.Value,示意 Go 语言中的值,其不仅蕴含变量的值,还蕴含该变量的类型信息。reflect.Value 定义非常简单,只蕴含三个属性:
type Value struct { | |
// 类型 | |
typ *rtype | |
// 指向数据首地址 | |
ptr unsafe.Pointer | |
// 标识,比方该变量只读,比方该变量是否蕴含执行数据的指针,比方该变量是否是一个办法等 | |
flag | |
} |
reflect.Value 构造也定义了十分多的办法,使得咱们能够很不便的判断 value 值是否可批改以及批改值,如果 value 值是一个办法,还能通过反射调用该办法;通过 reflect.Value 还能很不便的转化到 reflect.Type,而且 reflect.Value 自身也实现了一些 reflect.Type 接口中定义的办法(没有全副实现)。reflect.Value 的一些罕用办法如下:
// CanSet reports whether the value of v can be changed. | |
(v Value) CanSet() bool | |
// 获取 value 存储的值,批改 value 存储的值 | |
(v Value) Bool() bool | |
(v Value) Int() int64 | |
(v Value) SetBool(x bool) | |
(v Value) SetInt(x int64) | |
// Kind returns v's Kind. | |
(v Value) Kind() Kind | |
// Len returns v's length. | |
(v Value) Len() int | |
// 反射办法调用 | |
// For example, if len(in) == 3, v.Call(in) represents the Go call v(in[0], in[1], in[2]) | |
(v Value) Call(in []Value) []Value |
咱们以反射调用办法为例,学习 reflect.Value 的简略应用:
package main | |
import ( | |
"fmt" | |
"reflect" | |
) | |
func main() {f := func (a, b int) int {return a + b} | |
val := reflect.ValueOf(f) | |
ret := val.Call([]reflect.Value{reflect.ValueOf(100), reflect.ValueOf(200)}) | |
fmt.Println(ret[0].Int()) //300 | |
} |
变量 f 就是函数类型,所以咱们能通过 val.Call 调用;并且传递了两个整型输出参数,Call 办法返回 reflect.Value 切片,对应函数的多个返回值。看到这里你可能想说,这有什么意义?间接调用函数 f 不好么,为什么要这么简单。想说的是,有些场景的确适宜应用反射形式执行函数调用,比方 rpc 框架通常都这么做。
rpc 框架中的反射
rpc 即近程过程调用,客户端能够像调用本地办法一样调用远端服务的办法,rpc 框架如同桥梁个别连贯着客户端与远端服务。客户端的办法调用,rpc 框架将该调用过程序列化(能够自定义二进制协定,json 协定,甚至是 HTTP 协定序列化),序列化后的数据包含本地调用的服务名,办法名,以及输出参数。远端服务接管到客户端申请后,再通过反序列化,解析出客户端调用的服务名,办法名,以及输出参数,查找对应实现并执行,执行后果序列化之后再返回给客户端。
不思考序列化协定,想想 rpc 框架应该怎么设计?客户端申请达到,服务端解析出服务名,办法名,以及输出参数,接下来是不是该本地查找服务名与办法名对应的实现函数了,查到到实现函数后就是执行该函数了。服务端在启动之后,个别会注册本地服务 + 办法到全局 map,如 map[string]* methodType;申请达到之后查找到 method 之后,个别也是通过反射形式调用。
这里咱们以 github.com/smallnest/rpcx 框架为例,介绍其服务注册过程,以及反射执行申请的过程:
// 服务定义 | |
type service struct { | |
name string // 服务名称 | |
rcvr reflect.Value // 服务办法的接受者 | |
typ reflect.Type // 服务办法的接受者类型 | |
method map[string]*methodType // 所有注册的服务办法 | |
} | |
// 服务注册,rcvr 构造体指针类型,构造体的办法就是可对外提供服务的办法 | |
func (s *Server) register(rcvr interface{}) (string, error) {service := new(service) | |
service.typ = reflect.TypeOf(rcvr) | |
service.rcvr = reflect.ValueOf(rcvr) | |
service.name = reflect.Indirect(service.rcvr).Type().Name() | |
// 遍历构造体所有办法(校验办法定义是否非法)service.method = make(map[string]*methodType) | |
for m := 0; m < typ.NumMethod(); m++ {method := typ.Method(m) | |
mtype := method.Type | |
mname := method.Name | |
// 办法首字母小写时(不对外裸露),PkgPath 不为空,略过 | |
if method.PkgPath != "" {continue} | |
// 注册的办法都必须有四个输出参数: receiver, context.Context, *args, *reply. | |
if mtype.NumIn() != 4 { | |
if reportErr {log.Info("method", mname, "has wrong number of ins:", mtype.NumIn()) | |
} | |
continue | |
} | |
// 校验四个参数类型是否非法 | |
// 返回值必须是 error | |
var typeOfError = reflect.TypeOf((*error)(nil)).Elem() | |
if returnType := mtype.Out(0); returnType != typeOfError { | |
if reportErr {log.Info("method", mname, "returns", returnType.String(), "not error") | |
} | |
continue | |
} | |
// 校验通过;methodType 蕴含 method,参数类型(args),返回值类型(reply)service.method[mname] = &methodType{method: method, ArgType: argType, ReplyType: replyType} | |
} | |
// 保留 service 到全局 map | |
} |
能够看到,register 注册函数,传入的是构造体指针,服务名称也就是构造体名称;服务办法就是构造体的办法,只是该 rpc 框架对办法有一些限度,比方必须蕴含四个输出参数(办法接收者作为第 0 个参数,不思考在内)第一个参数类型必须是 context.Context,第三个参数必须是指针类型(要返回后果,Go 语言按值传递,指针类型能力批改输出参数),而且办法必须返回 error 类型。
留神校验构造体的办法时,还判断了 method.PkgPath(PkgPath is the package path that qualifies a lower case (unexported) method name);小写,未裸露什么意思呢?第一篇文章简略提过,Go 语言所有文件都必须指定其所在的包,如上 ”package main”,咱们称之为 main 包,当然包名也能够命名为其余名称(个别包名与以后所在目录 / 文件夹名称保持一致),而 main 包里的 main 函数为程序的入口函数。咱们的代码必定会仍然其余文件,怎么引入呢?通过 ”import 包名 ” 引入,引入后能力应用该包内函数 / 变量 / 申明的类型。其实还有一些限度,通过 ”import 包名 ” 引入其余包之后,只能应用其局部函数或者变量或者定义的类型:首字母大写命名的。所以才说,首字母小写的办法 unexported。
那么,服务端接管到客户端的 rpc 申请之后,如何查找匹配对应办法并执行呢?咱们同样以 rpcx 框架为例:
func (s *Server) handleRequest(ctx context.Context, req *protocol.Message) (res *protocol.Message, err error) { | |
//ServicePath 申请的服务 | |
service := s.serviceMap[req.ServicePath] | |
//ServiceMethod 申请的办法 | |
mtype := service.method[req.ServiceMethod] | |
// 依据参数类型创立变量 | |
var argv = rflect.New(mtype.ArgType) | |
// 依据参数类型,反序列化解析申请 body | |
err = codec.Decode(req.Payload, argv) | |
// 依据返回参数类型创立变量 | |
replyv := rflect.New(mtype.ReplyType) | |
// 反射调用 | |
function := mtype.method.Func | |
function.Call([]reflect.Value{s.rcvr, reflect.ValueOf(ctx), reflect.ValueOf(argv), reflect.ValueOf(replyv)}) | |
// 序列化返回参数变量到 []byte | |
data, err := codec.Encode(replyv) | |
} |
留神反射执行办法时,输出参数都是 reflect.Value 类型,晓得了参数类型,能够通过 rflect.New 创立该类型变量,而有了变量很容易通过 reflect.ValueOf 转化为 reflect.Value 类型。另外,rpc 框架个别反对多种序列化协定,如自定义二进制协定,json 协定,甚至 HTTP 协定;反射形式执行办法时,依据约定好的不同序列化协定解析申请参数,以及编码返回后果。
总结
反射使得 Go 语言具备一些动静个性,当你不分明函数参数类型时,能够定义为 interface{} 类型,函数内通过反射等形式依据不同类型执行不同逻辑。Go 语言反射相干都定义在 reflect 包,其中最重要的就是 reflect.Type 以及 reflect.Value,本篇文章列出了一些罕用函数 / 办法。最初以 rpcx 框架为例,介绍了反射在 rpc 框架中服务办法注册,以及服务办法执行过程的应用。