Go 编程:图解反射

原文发布在个人站点: GitDiG.com, 原文链接:https://www.gitdig.com/go-reflect/

1. 图解反射

在使用反射之前,此文The Laws of Reflection必读。网上中文翻译版本不少,可以搜索阅读。

开始具体篇幅之前,先看一下反射三原则:

  • Reflection goes from interface value to reflection object.
  • Reflection goes from reflection object to interface value.
  • To modify a reflection object, the value must be settable.

在三原则中,有两个关键词 interface valuereflection object。有点难理解,画张图可能你就懂了。

reflect.png

先看一下什么是反射对象 reflection object? 反射对象有很多,但是其中最关键的两个反射对象reflection object是:reflect.Typereflect.Value.直白一点,就是对变量类型的抽象定义类,也可以说是变量的元信息的类定义.

再来,为什么是接口变量值 interface value, 不是变量值 variable value 或是对象值 object value 呢?因为后两者均不具备广泛性。在 Go 语言中,空接口 interface{}是可以作为一切类型值的通用类型使用。所以这里的接口值 interface value 可以理解为空接口变量值 interface{} value

结合图示,将反射三原则归纳成一句话:

通过反射可以实现反射对象 reflection object接口变量值 interface value之间的相互推导与转化, 如果通过反射修改对象变量的值,前提是对象变量本身是可修改的。

2. 反射的应用

在程序开发中是否需要使用反射功能,判断标准很简单,即是否需要用到变量的类型信息。这点不难判断,如何合理的使用反射才是难点。因为,反射不同于普通的功能函数,它对程序的性能是有损耗的,需要尽量避免在高频操作中使用反射。

举几个反射应用的场景例子:

2.1 判断未知对象是否实现具体接口

通常情况下,判断未知对象是否实现具体接口很简单,直接通过 变量名.(接口名) 类型验证的方式就可以判断。但是有例外,即框架代码实现中检查调用代码的情况。因为框架代码先实现,调用代码后实现,也就无法在框架代码中通过简单额类型验证的方式进行验证。

看看 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框架的基础实现,因为这段代码通常会是在程序的启动阶段所以对于程序的性能而言没有任何影响。

2.2 结构体字段属性标签

通常定义一个待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,完全是参考该库的实现写的。

2.3 函数适配

原来做练习的时候,写过一段函数适配的代码,用到反射。贴一下:

//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出了,验证一下。

3 小结

反射的最佳应用场景是程序的启动阶段,实现一些类型检查、注册等前置工作,既不影响程序性能同时又增加了代码的可读性。最近迷上新裤子,所以别再问我什么是反射了:)

参考资源:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容