原文发布在个人我的站点: Go 编程:图解反射git
反射三原则太难理解,看一张图你就懂了。完美解释两个关键词 interface value 与 reflection object 是什么。github
在使用反射以前,此文The Laws of Reflection必读。网上中文翻译版本很多,能够搜索阅读。golang
开始具体篇幅以前,先看一下反射三原则:编程
在三原则中,有两个关键词 interface value
与 reflection object
。有点难理解,画张图可能你就懂了。app
先看一下什么是反射对象 reflection object
? 反射对象有不少,可是其中最关键的两个反射对象reflection object
是:reflect.Type
与reflect.Value
.直白一点,就是对变量类型
与值
的抽象定义类,也能够说是变量的元信息的类定义.框架
再来,为何是接口变量值 interface value
, 不是变量值 variable value
或是对象值 object value
呢?由于后二者均不具有普遍性。在 Go 语言中,空接口 interface{}
是能够做为一切类型值的通用类型使用。因此这里的接口值 interface value
能够理解为空接口变量值 interface{} value
。ide
结合图示,将反射三原则概括成一句话:函数
经过反射能够实现反射对象
reflection object
与接口变量值interface value
之间的相互推导与转化, 若是经过反射修改对象变量的值,前提是对象变量自己是可修改
的。性能
在程序开发中是否须要使用反射功能,判断标准很简单,便是否须要用到变量的类型信息。这点不难判断,如何合理的使用反射才是难点。由于,反射不一样于普通的功能函数,它对程序的性能是有损耗的,须要尽可能避免在高频操做中使用反射。ui
举几个反射应用的场景例子:
一般状况下,判断未知对象是否实现具体接口很简单,直接经过 变量名.(接口名)
类型验证的方式就能够判断。可是有例外,即框架代码实现中检查调用代码的状况。由于框架代码先实现,调用代码后实现,也就没法在框架代码中经过简单额类型验证的方式进行验证。
看看 grpc
的服务端注册接口就明白了。
grpcServer := grpc.NewServer()
// 服务端实现注册
pb.RegisterRouteGuideServer(grpcServer, &routeGuideServer{})
复制代码
当注册的实现没有实现全部的服务接口时,程序就会报错。它是如何作的,能够直接查看pb.RegisterRouteGuideServer
的实现代码。这里简单的写一段代码,原理相同:
//目标接口定义
type Foo interface {
Bar(int)
}
dst := (*Foo)(nil)
dstType := reflect.TypeOf(dst).Elem()
//验证未知变量 src 是否实现 Foo 目标接口
srcType := reflect.TypeOf(src)
if !srcType.Implements(dstType) {
log.Fatalf("type %v that does not satisfy %v", srcType, dstType)
}
复制代码
这也是grpc
框架的基础实现,由于这段代码一般会是在程序的启动阶段因此对于程序的性能而言没有任何影响。
一般定义一个待JSON解析的结构体时,会对结构体中具体的字段属性进行tag
标签设置,经过tag
的辅助信息对应具体JSON字符串对应的字段名。JSON解析就不提供例子了,并且一般JSON解析代码会做用于请求响应阶段,并不是反射的最佳场景,可是业务上又不得不这么作。
这里我要引用另一个利用结构体字段属性标签作反射的例子,也是我认为最完美诠释反射的例子,真的很是值得推荐。这个例子出如今开源项目github.com/jaegertracing/jaeger-lib
中。
用过 prometheus
的同窗都知道,metric
探测标量是须要经过如下过程定义并注册的:
var (
// Create a summary to track fictional interservice RPC latencies for three
// distinct services with different latency distributions. These services are
// differentiated via a "service" label.
rpcDurations = prometheus.NewSummaryVec(
prometheus.SummaryOpts{
Name: "rpc_durations_seconds",
Help: "RPC latency distributions.",
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
},
[]string{"service"},
)
// The same as above, but now as a histogram, and only for the normal
// distribution. The buckets are targeted to the parameters of the
// normal distribution, with 20 buckets centered on the mean, each
// half-sigma wide.
rpcDurationsHistogram = prometheus.NewHistogram(prometheus.HistogramOpts{
Name: "rpc_durations_histogram_seconds",
Help: "RPC latency distributions.",
Buckets: prometheus.LinearBuckets(*normMean-5**normDomain, .5**normDomain, 20),
})
)
func init() {
// Register the summary and the histogram with Prometheus's default registry.
prometheus.MustRegister(rpcDurations)
prometheus.MustRegister(rpcDurationsHistogram)
// Add Go module build info.
prometheus.MustRegister(prometheus.NewBuildInfoCollector())
}
复制代码
这是 prometheus/client_golang
提供的例子,代码量多,并且须要使用init
函数。项目一旦复杂,可读性就不好。再看看github.com/jaegertracing/jaeger-lib/metrics
提供的方式:
type App struct{
//attributes ...
//metrics ...
metrics struct{
// Size of the current server queue
QueueSize metrics.Gauge `metric:"thrift.udp.server.queue_size"`
// Size (in bytes) of packets received by server
PacketSize metrics.Gauge `metric:"thrift.udp.server.packet_size"`
// Number of packets dropped by server
PacketsDropped metrics.Counter `metric:"thrift.udp.server.packets.dropped"`
// Number of packets processed by server
PacketsProcessed metrics.Counter `metric:"thrift.udp.server.packets.processed"`
// Number of malformed packets the server received
ReadError metrics.Counter `metric:"thrift.udp.server.read.errors"`
}
}
复制代码
在应用中首先直接定义匿名结构metrics
, 将针对该应用的metric
探测标量定义到具体的结构体字段中,并经过其字段标签tag
的方式设置名称。这样在代码的可读性大大加强了。
再看看初始化代码:
import "github.com/jaegertracing/jaeger-lib/metrics/prometheus"
//初始化
metrics.Init(&app.metrics, prometheus.New(), nil)
复制代码
不服不行,完美。这段样例代码实如今个人这个项目中: x-mod/thriftudp,彻底是参考该库的实现写的。
原来作练习的时候,写过一段函数适配的代码,用到反射。贴一下:
//Executor 适配目标接口,增长 context.Context 参数
type Executor func(ctx context.Context, args ...interface{}) //Adapter 适配器适配任意函数 func Adapter(fn interface{}) Executor {
if fn != nil && reflect.TypeOf(fn).Kind() == reflect.Func {
return func(ctx context.Context, args ...interface{}) {
fv := reflect.ValueOf(fn)
params := make([]reflect.Value, 0, len(args)+1)
params = append(params, reflect.ValueOf(ctx))
for _, arg := range args {
params = append(params, reflect.ValueOf(arg))
}
fv.Call(params)
}
}
return func(ctx context.Context, args ...interface{}) {
log.Warn("null executor implemention")
}
}
复制代码
仅仅为了练习,生产环境仍是不推荐使用,感受过重了。
最近看了一下Go 1.14
的提案,关于try
关键字的引入, try参考。按其所展现的功能,若是本身实现的话,应该会用到反射功能。那么对于如今如此依赖 error
检查的函数实现来讲,是否合适,挺怀疑的,等Go 1.14
出了,验证一下。
反射的最佳应用场景是程序的启动阶段,实现一些类型检查、注册等前置工做,既不影响程序性能同时又增长了代码的可读性。最近迷上新裤子,因此别再问我什么是反射了:)