

【1-6 Golang】Go语言快速入门—反射
source link: https://studygolang.com/articles/35876
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

反射使得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框架中服务方法注册,以及服务方法执行过程的使用。
Recommend
-
29
接口 接着上次的继续讲接口,先回顾一下接口的用法: package main import "fmt" // 定义接口 type Car interface { GetName() string Run() } // 定义结构体 type Tesla struct { Name string } /...
-
68
Go 语言反射的实现原理4.3 反射反射是 Go 语言比较重要的特性。虽然在大多数的应用和服务中并不常见,但是很多框架都依赖 Go 语言的反射机制实现简化代码的逻辑。因为 Go 语言的语法元素很少、设计简单,...
-
32
golang-gopher.png 1. 概述 Go语言提供了一种机制,能够在运行时更新变量和检查它们的值、调...
-
27
macos安装Go语言开发包 配置go语言的开发环境的第一步是要在 go官网下载页面...
-
5
【1-1 Golang】Go语言快速入门—基本语法 tomato01 · 大约3小时之前 · 137...
-
11
【1-2 Golang】Go语言快速入门—数组与切片 tomato01 · 大约15小时之前 ·...
-
7
【1-3 Golang】Go语言快速入门—字符串 tomato01 · 大约7小时之前 · 117...
-
5
【1-4 Golang】Go语言快速入门—哈希表MAP tomato01 · 大约8小时之前 · 5...
-
4
【1-5 Golang】Go语言快速入门—结构体与接口 tomato01 · 1天之前 · 295...
-
7
【1-7 Golang】Go语言快速入门—泛型 tomato01 · 大约23小时之前 · 298 次...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK