教程集 www.jiaochengji.com
教程集 >  Golang编程  >  golang教程  >  正文 Go 编程:图解反射

Go 编程:图解反射

发布时间:2022-01-15   编辑:jiaochengji.com
教程集为您提供Go 编程:图解反射等资源,欢迎您收藏本站,我们将为您提供最新的Go 编程:图解反射资源
<blockquote>

原文发布在我的个人站点: Go 编程:图解反射

</blockquote>

反射三原则太难理解,看一张图你就懂了。完美解释两个关键词 interface value 与 reflection object 是什么。

<h2 class="heading" data-id="heading-0">1. 图解反射</h2>

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

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

<ul><li>Reflection goes from interface value to reflection object.</li> <li>Reflection goes from reflection object to interface value.</li> <li>To modify a reflection object, the value must be settable.</li> </ul>

在三原则中,有两个关键词 <code>interface value</code> 与 <code>reflection object</code>。有点难理解,画张图可能你就懂了。

<figure><figcaption/></figure>

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

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

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

<blockquote>

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

</blockquote> <h2 class="heading" data-id="heading-1">2. 反射的应用</h2>

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

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

<h3 class="heading" data-id="heading-2">2.1 判断未知对象是否实现具体接口</h3>

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

看看 <code>grpc</code> 的服务端注册接口就明白了。

<pre><code class="lang-hljs go copyable" lang="go hljs">grpcServer := grpc.NewServer() <span class="hljs-comment">// 服务端实现注册</span> pb.RegisterRouteGuideServer(grpcServer, &routeGuideServer{}) <span class="copy-code-btn">复制代码</span></code></code></pre>

当注册的实现没有实现所有的服务接口时,程序就会报错。它是如何做的,可以直接查看<code>pb.RegisterRouteGuideServer</code>的实现代码。这里简单的写一段代码,原理相同:

<pre><code class="lang-hljs go copyable" lang="go hljs"> <span class="hljs-comment">//目标接口定义</span> <span class="hljs-keyword">type</span> Foo <span class="hljs-keyword">interface</span> { Bar(<span class="hljs-keyword">int</span>) } dst := (*Foo)(<span class="hljs-literal">nil</span>) dstType := reflect.TypeOf(dst).Elem() <span class="hljs-comment">//验证未知变量 src 是否实现 Foo 目标接口</span> srcType := reflect.TypeOf(src) <span class="hljs-keyword">if</span> !srcType.Implements(dstType) { log.Fatalf(<span class="hljs-string">"type %v that does not satisfy %v"</span>, srcType, dstType) } <span class="copy-code-btn">复制代码</span></code></code></pre>

这也是<code>grpc</code>框架的基础实现,因为这段代码通常会是在程序的启动阶段所以对于程序的性能而言没有任何影响。

<h3 class="heading" data-id="heading-3">2.2 结构体字段属性标签</h3>

通常定义一个待JSON解析的结构体时,会对结构体中具体的字段属性进行<code>tag</code>标签设置,通过<code>tag</code>的辅助信息对应具体JSON字符串对应的字段名。JSON解析就不提供例子了,而且通常JSON解析代码会作用于请求响应阶段,并非反射的最佳场景,但是业务上又不得不这么做。

这里我要引用另外一个利用结构体字段属性标签做反射的例子,也是我认为最完美诠释反射的例子,真的非常值得推荐。这个例子出现在开源项目<code>github.com/jaegertracing/jaeger-lib</code>中。

用过 <code>prometheus</code>的同学都知道,<code>metric</code>探测标量是需要通过以下过程定义并注册的:

<pre><code class="lang-hljs go copyable" lang="go hljs"><span class="hljs-keyword">var</span> ( <span class="hljs-comment">// Create a summary to track fictional interservice RPC latencies for three</span> <span class="hljs-comment">// distinct services with different latency distributions. These services are</span> <span class="hljs-comment">// differentiated via a "service" label.</span> rpcDurations = prometheus.NewSummaryVec( prometheus.SummaryOpts{ Name: <span class="hljs-string">"rpc_durations_seconds"</span>, Help: <span class="hljs-string">"RPC latency distributions."</span>, Objectives: <span class="hljs-keyword">map</span>[<span class="hljs-keyword">float64</span>]<span class="hljs-keyword">float64</span>{<span class="hljs-number">0.5</span>: <span class="hljs-number">0.05</span>, <span class="hljs-number">0.9</span>: <span class="hljs-number">0.01</span>, <span class="hljs-number">0.99</span>: <span class="hljs-number">0.001</span>}, }, []<span class="hljs-keyword">string</span>{<span class="hljs-string">"service"</span>}, ) <span class="hljs-comment">// The same as above, but now as a histogram, and only for the normal</span> <span class="hljs-comment">// distribution. The buckets are targeted to the parameters of the</span> <span class="hljs-comment">// normal distribution, with 20 buckets centered on the mean, each</span> <span class="hljs-comment">// half-sigma wide.</span> rpcDurationsHistogram = prometheus.NewHistogram(prometheus.HistogramOpts{ Name: <span class="hljs-string">"rpc_durations_histogram_seconds"</span>, Help: <span class="hljs-string">"RPC latency distributions."</span>, Buckets: prometheus.LinearBuckets(*normMean<span class="hljs-number">-5</span>**normDomain, <span class="hljs-number">.5</span>**normDomain, <span class="hljs-number">20</span>), }) ) <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">init</span><span class="hljs-params">()</span></span> { <span class="hljs-comment">// Register the summary and the histogram with Prometheus's default registry.</span> prometheus.MustRegister(rpcDurations) prometheus.MustRegister(rpcDurationsHistogram) <span class="hljs-comment">// Add Go module build info.</span> prometheus.MustRegister(prometheus.NewBuildInfoCollector()) } <span class="copy-code-btn">复制代码</span></code></code></pre>

这是 <code>prometheus/client_golang</code> 提供的例子,代码量多,而且需要使用<code>init</code>函数。项目一旦复杂,可读性就很差。再看看<code>github.com/jaegertracing/jaeger-lib/metrics</code>提供的方式:

<pre><code class="lang-hljs go copyable" lang="go hljs"><span class="hljs-keyword">type</span> App <span class="hljs-keyword">struct</span>{ <span class="hljs-comment">//attributes ...</span> <span class="hljs-comment">//metrics ...</span> metrics <span class="hljs-keyword">struct</span>{ <span class="hljs-comment">// Size of the current server queue</span> QueueSize metrics.Gauge <span class="hljs-string">`metric:"thrift.udp.server.queue_size"`</span> <span class="hljs-comment">// Size (in bytes) of packets received by server</span> PacketSize metrics.Gauge <span class="hljs-string">`metric:"thrift.udp.server.packet_size"`</span> <span class="hljs-comment">// Number of packets dropped by server</span> PacketsDropped metrics.Counter <span class="hljs-string">`metric:"thrift.udp.server.packets.dropped"`</span> <span class="hljs-comment">// Number of packets processed by server</span> PacketsProcessed metrics.Counter <span class="hljs-string">`metric:"thrift.udp.server.packets.processed"`</span> <span class="hljs-comment">// Number of malformed packets the server received</span> ReadError metrics.Counter <span class="hljs-string">`metric:"thrift.udp.server.read.errors"`</span> } } <span class="copy-code-btn">复制代码</span></code></code></pre>

在应用中首先直接定义匿名结构<code>metrics</code>, 将针对该应用的<code>metric</code>探测标量定义到具体的结构体字段中,并通过其字段标签<code>tag</code>的方式设置名称。这样在代码的可读性大大增强了。

再看看初始化代码:

<pre><code class="lang-hljs go copyable" lang="go hljs"><span class="hljs-keyword">import</span> <span class="hljs-string">"github.com/jaegertracing/jaeger-lib/metrics/prometheus"</span> <span class="hljs-comment">//初始化</span> metrics.Init(&app.metrics, prometheus.New(), <span class="hljs-literal">nil</span>) <span class="copy-code-btn">复制代码</span></code></code></pre>

不服不行,完美。这段样例代码实现在我的这个项目中: x-mod/thriftudp,完全是参考该库的实现写的。

<h3 class="heading" data-id="heading-4">2.3 函数适配</h3>

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

<pre><code class="lang-hljs go copyable" lang="go hljs"><span class="hljs-comment">//Executor 适配目标接口,增加 context.Context 参数</span> <span class="hljs-keyword">type</span> Executor <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx context.Context, args ...<span class="hljs-keyword">interface</span>{})</span> //<span class="hljs-title">Adapter</span> 适配器适配任意函数 <span class="hljs-title">func</span> <span class="hljs-title">Adapter</span><span class="hljs-params">(fn <span class="hljs-keyword">interface</span>{})</span> <span class="hljs-title">Executor</span></span> { <span class="hljs-keyword">if</span> fn != <span class="hljs-literal">nil</span> && reflect.TypeOf(fn).Kind() == reflect.Func { <span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx context.Context, args ...<span class="hljs-keyword">interface</span>{})</span></span> { fv := reflect.ValueOf(fn) params := <span class="hljs-built_in">make</span>([]reflect.Value, <span class="hljs-number">0</span>, <span class="hljs-built_in">len</span>(args) <span class="hljs-number">1</span>) params = <span class="hljs-built_in">append</span>(params, reflect.ValueOf(ctx)) <span class="hljs-keyword">for</span> _, arg := <span class="hljs-keyword">range</span> args { params = <span class="hljs-built_in">append</span>(params, reflect.ValueOf(arg)) } fv.Call(params) } } <span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx context.Context, args ...<span class="hljs-keyword">interface</span>{})</span></span> { log.Warn(<span class="hljs-string">"null executor implemention"</span>) } } <span class="copy-code-btn">复制代码</span></code></code></pre>

仅仅为了练习,生产环境还是不推荐使用,感觉太重了。

最近看了一下<code>Go 1.14</code>的提案,关于<code>try</code>关键字的引入, try参考。按其所展示的功能,如果自己实现的话,应该会用到反射功能。那么对于现在如此依赖 <code>error</code> 检查的函数实现来说,是否合适,挺怀疑的,等<code>Go 1.14</code>出了,验证一下。

<h2 class="heading" data-id="heading-5">3 小结</h2>

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

<h2 class="heading" data-id="heading-6">参考资源:</h2> <ul><li>The Laws of Reflection</li> <li>Go Data Structures: Interfaces</li> </ul>
到此这篇关于“Go 编程:图解反射”的文章就介绍到这了,更多文章或继续浏览下面的相关文章,希望大家以后多多支持JQ教程网!

您可能感兴趣的文章:
图解 Go 反射实现原理
Go语言学习之reflect包(The way to go)
golang 映射 map 简介
golang的反射机制与实践(上)
Go 编程:图解反射
Go 反射机制介绍
c#反射调用类型成员的例子
想系统学习GO语言(Golang
go 语言学习历程
GO语言零基础从入门到精通视频教程

[关闭]
~ ~