【1-6 Golang】Go语言快速入门—反射

tomato01 · · 627 次点击 · · 开始浏览    

&emsp;&emsp;反射使得Go语言具备一些动态特性,比如不知道参数类型怎么办?当然你可以定义多个函数,分别传递不同参数;你也可以定义一个函数就行,参数类型为interface{},函数内通过反射操作变量。一些rpc框架,通常使用反射注册服务方法,以及通过反射调用服务方法。 ## 反射初体验 &emsp;&emsp;如何使用反射呢?我们以字符串转化函数为例,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() } ``` &emsp;&emsp;初次看这段代码,可能会不知所云,reflect.TypeOf是什么不了解,reflect.ValueOf也看不懂,v.Type().Implements又有什么作用,等等等等;这些其实都是反射常用的一些方法以及套路。Go语言反射标准库定义在包reflect,最常用reflect.Type,表示Go语言类型,这是一个接口,定义了很多方法,可以帮助我们获取到该类型拥有的属性、方法,占用内存大小等等;以及reflect.Value,表示Go语言中的值,其包含变量的值,以及该变量的类型信息。 &emsp;&emsp;什么类型变量可以转化为字符串呢?int,float,[]byte等都肯定都是可以的;除了这些,部分接口也是可以的,如fmt.Stringer接口,如error接口,其都定义了方法可以返回该类型字符串描述;另外对于指针类型,想转化为字符串,肯定先要获取到其指向的元素类型以及值才行。 ``` type Stringer interface { String() string } type error interface { Error() string } ``` &emsp;&emsp;上述程序用到的几个函数/方法的定义如下: ``` // 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{}) ``` &emsp;&emsp;明白了这些函数/方法的含义之后,上述程序应该可以理解了:通过将nil强制转化为error指针类型方式,获取error接口类型;通过将nil强制转化为fmt.Stringer指针类型方式,获取fmt.Stringer接口类型;如果当前变量没有实现fmt.Stringer接口,也没有实现error接口,并且是指针类型,则获取获取到其指向的元素类型。最后将value值包装为空接口类型返回。主函数ToStringE再根据变量类型,走不同的字符串转化逻辑。 ## reflect.Type &emsp;&emsp;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 } ``` &emsp;&emsp;这里只列出了部分函数的定义以及注释说明,还有部分函数没有给出,读者可以查阅reflect包。另外注意,很多方法只适合某些类型,比如函数相关,要求类型必须是funcType,一旦类型不对,就会抛panic异常。 &emsp;&emsp;上一篇文章讲解结构体时提到,Go语言所有类型,都有其对应的类型定义。runtime/type.go文件定义了最基本的类型_type struct;其余类型,如切片类型slicetype,map类型maptype,函数类型functype,等等都继承自_type。与之对应的,反射reflect包也定义了所有类型,如rtype(与_type对应),切片类型sliceType,map类型mapType,函数类型funcType。runtime包定义的诸多类型其实与reflect包定义的诸多类型都是一一对应的。 &emsp;&emsp;我们简单了解下一些常用类型的定义: ``` //结构体,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 ... } ``` &emsp;&emsp;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 ``` &emsp;&emsp;看到了吧,通过反射访问结构体定义的属性以及方法还是比较简单的,其余类型也非常类似,这里就不再赘述。至于底层是如何获取到结构体的各属性以及方法,研究下上面介绍的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] } ``` &emsp;&emsp;看到这里,思考下Implements方法怎么实现呢?是不是也没那么神秘,其实就是遍历接口类型的所有方法,判断结构体类型是否定义了,如果没有则说明没有实现该接口。最后,有兴趣的读者可以自己研究下每一种类型的定义,以及反射操作方法,以及这些方法的实现原理。 ## reflect.Value &emsp;&emsp;reflect.Value,表示Go语言中的值,其不仅包含变量的值,还包含该变量的类型信息。reflect.Value定义非常简单,只包含三个属性: ``` type Value struct { // 类型 typ *rtype // 指向数据首地址 ptr unsafe.Pointer //标识,比如该变量只读,比如该变量是否包含执行数据的指针,比如该变量是否是一个方法等 flag } ``` &emsp;&emsp;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 ``` &emsp;&emsp;我们以反射调用方法为例,学习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 } ``` &emsp;&emsp;变量f就是函数类型,所以我们能通过val.Call调用;并且传递了两个整型输入参数,Call方法返回reflect.Value切片,对应函数的多个返回值。看到这里你可能想说,这有什么意义?直接调用函数f不好么,为什么要这么复杂。想说的是,有些场景确实适合使用反射方式执行函数调用,比如rpc框架通常都这么做。 ## rpc框架中的反射 &emsp;&emsp;rpc即远程过程调用,客户端可以像调用本地方法一样调用远端服务的方法,rpc框架如同桥梁一般连接着客户端与远端服务。客户端的方法调用,rpc框架将该调用过程序列化(可以自定义二进制协议,json协议,甚至是HTTP协议序列化),序列化后的数据包括本地调用的服务名,方法名,以及输入参数。远端服务接收到客户端请求后,再通过反序列化,解析出客户端调用的服务名,方法名,以及输入参数,查找对应实现并执行,执行结果序列化之后再返回给客户端。 &emsp;&emsp;不考虑序列化协议,想想rpc框架应该怎么设计?客户端请求到达,服务端解析出服务名,方法名,以及输入参数,接下来是不是该本地查找服务名与方法名对应的实现函数了,查到到实现函数后就是执行该函数了。服务端在启动之后,一般会注册本地服务+方法到全局map,如map[string]* methodType;请求到达之后查找到method之后,一般也是通过反射方式调用。 &emsp;&emsp;这里我们以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 } ``` &emsp;&emsp;可以看到,register注册函数,传入的是结构体指针,服务名称也就是结构体名称;服务方法就是结构体的方法,只是该rpc框架对方法有一些限制,比如必须包含四个输入参数(方法接收者作为第0个参数,不考虑在内)第一个参数类型必须是context.Context,第三个参数必须是指针类型(要返回结果,Go语言按值传递,指针类型才能修改输入参数),而且方法必须返回error类型。 &emsp;&emsp;注意校验结构体的方法时,还判断了method.PkgPath(PkgPath is the package path that qualifies a lower case (unexported) method name);小写,未暴露什么意思呢?第一篇文章简单提过,Go语言所有文件都必须指定其所在的包,如上"package main",我们称之为main包,当然包名也可以命名为其他名称(一般包名与当前所在目录/文件夹名称保持一致),而main包里的main函数为程序的入口函数。我们的代码肯定会依然其他文件,怎么引入呢?通过"import 包名"引入,引入后才能使用该包内函数/变量/声明的类型。其实还有一些限制,通过"import 包名"引入其他包之后,只能使用其部分函数或者变量或者定义的类型:首字母大写命名的。所以才说,首字母小写的方法unexported。 &emsp;&emsp;那么,服务端接收到客户端的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) } ``` &emsp;&emsp;注意反射执行方法时,输入参数都是reflect.Value类型,知道了参数类型,可以通过rflect.New创建该类型变量,而有了变量很容易通过reflect.ValueOf转化为reflect.Value类型。另外,rpc框架一般支持多种序列化协议,如自定义二进制协议,json协议,甚至HTTP协议;反射方式执行方法时,根据约定好的不同序列化协议解析请求参数,以及编码返回结果。 ## 总结 &emsp;&emsp;反射使得Go语言具备一些动态特性,当你不清楚函数参数类型时,可以定义为interface{}类型,函数内通过反射等方式根据不同类型执行不同逻辑。Go语言反射相关都定义在reflect包,其中最重要的就是reflect.Type以及reflect.Value,本篇文章列出了一些常用函数/方法。最后以rpcx框架为例,介绍了反射在rpc框架中服务方法注册,以及服务方法执行过程的使用。

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

627 次点击  
加入收藏 微博
1 回复  |  直到 2022-09-21 23:56:11
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传