原文發(fā)布在個人站點: GitDiG.com, 原文鏈接:https://www.gitdig.com/go-reflect/
1. 圖解反射
在使用反射之前痰洒,此文The Laws of Reflection必讀醋寝。網(wǎng)上中文翻譯版本不少,可以搜索閱讀带迟。
開始具體篇幅之前音羞,先看一下反射三原則:
- 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.
在三原則中,有兩個關(guān)鍵詞 interface value
與 reflection object
仓犬。有點難理解嗅绰,畫張圖可能你就懂了。
先看一下什么是反射對象 reflection object
搀继? 反射對象有很多窘面,但是其中最關(guān)鍵的兩個反射對象reflection object
是:reflect.Type
與reflect.Value
.直白一點,就是對變量類型
與值
的抽象定義類叽躯,也可以說是變量的元信息的類定義.
再來财边,為什么是接口變量值 interface value
, 不是變量值 variable value
或是對象值 object value
呢?因為后兩者均不具備廣泛性点骑。在 Go 語言中酣难,空接口 interface{}
是可以作為一切類型值的通用類型使用。所以這里的接口值 interface value
可以理解為空接口變量值 interface{} value
黑滴。
結(jié)合圖示憨募,將反射三原則歸納成一句話:
通過反射可以實現(xiàn)反射對象
reflection object
與接口變量值interface value
之間的相互推導(dǎo)與轉(zhuǎn)化, 如果通過反射修改對象變量的值,前提是對象變量本身是可修改
的袁辈。
2. 反射的應(yīng)用
在程序開發(fā)中是否需要使用反射功能菜谣,判斷標(biāo)準(zhǔn)很簡單,即是否需要用到變量的類型信息晚缩。這點不難判斷尾膊,如何合理的使用反射才是難點。因為荞彼,反射不同于普通的功能函數(shù)冈敛,它對程序的性能是有損耗的,需要盡量避免在高頻操作中使用反射卿泽。
舉幾個反射應(yīng)用的場景例子:
2.1 判斷未知對象是否實現(xiàn)具體接口
通常情況下莺债,判斷未知對象是否實現(xiàn)具體接口很簡單滋觉,直接通過 變量名.(接口名)
類型驗證的方式就可以判斷。但是有例外齐邦,即框架代碼實現(xiàn)中檢查調(diào)用代碼的情況椎侠。因為框架代碼先實現(xiàn),調(diào)用代碼后實現(xiàn)措拇,也就無法在框架代碼中通過簡單額類型驗證的方式進(jìn)行驗證我纪。
看看 grpc
的服務(wù)端注冊接口就明白了。
grpcServer := grpc.NewServer()
// 服務(wù)端實現(xiàn)注冊
pb.RegisterRouteGuideServer(grpcServer, &routeGuideServer{})
當(dāng)注冊的實現(xiàn)沒有實現(xiàn)所有的服務(wù)接口時丐吓,程序就會報錯浅悉。它是如何做的,可以直接查看pb.RegisterRouteGuideServer
的實現(xiàn)代碼券犁。這里簡單的寫一段代碼术健,原理相同:
//目標(biāo)接口定義
type Foo interface {
Bar(int)
}
dst := (*Foo)(nil)
dstType := reflect.TypeOf(dst).Elem()
//驗證未知變量 src 是否實現(xiàn) Foo 目標(biāo)接口
srcType := reflect.TypeOf(src)
if !srcType.Implements(dstType) {
log.Fatalf("type %v that does not satisfy %v", srcType, dstType)
}
這也是grpc
框架的基礎(chǔ)實現(xiàn),因為這段代碼通常會是在程序的啟動階段所以對于程序的性能而言沒有任何影響粘衬。
2.2 結(jié)構(gòu)體字段屬性標(biāo)簽
通常定義一個待JSON解析的結(jié)構(gòu)體時荞估,會對結(jié)構(gòu)體中具體的字段屬性進(jìn)行tag
標(biāo)簽設(shè)置,通過tag
的輔助信息對應(yīng)具體JSON字符串對應(yīng)的字段名稚新。JSON解析就不提供例子了勘伺,而且通常JSON解析代碼會作用于請求響應(yīng)階段,并非反射的最佳場景褂删,但是業(yè)務(wù)上又不得不這么做飞醉。
這里我要引用另外一個利用結(jié)構(gòu)體字段屬性標(biāo)簽做反射的例子,也是我認(rèn)為最完美詮釋反射的例子屯阀,真的非常值得推薦缅帘。這個例子出現(xiàn)在開源項目github.com/jaegertracing/jaeger-lib
中。
用過 prometheus
的同學(xué)都知道蹲盘,metric
探測標(biāo)量是需要通過以下過程定義并注冊的:
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
函數(shù)召衔。項目一旦復(fù)雜,可讀性就很差祭陷。再看看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"`
}
}
在應(yīng)用中首先直接定義匿名結(jié)構(gòu)metrics
苍凛, 將針對該應(yīng)用的metric
探測標(biāo)量定義到具體的結(jié)構(gòu)體字段中,并通過其字段標(biāo)簽tag
的方式設(shè)置名稱兵志。這樣在代碼的可讀性大大增強(qiáng)了醇蝴。
再看看初始化代碼:
import "github.com/jaegertracing/jaeger-lib/metrics/prometheus"
//初始化
metrics.Init(&app.metrics, prometheus.New(), nil)
不服不行,完美想罕。這段樣例代碼實現(xiàn)在我的這個項目中: x-mod/thriftudp悠栓,完全是參考該庫的實現(xiàn)寫的霉涨。
2.3 函數(shù)適配
原來做練習(xí)的時候,寫過一段函數(shù)適配的代碼惭适,用到反射笙瑟。貼一下:
//Executor 適配目標(biāo)接口,增加 context.Context 參數(shù)
type Executor func(ctx context.Context, args ...interface{})
//Adapter 適配器適配任意函數(shù)
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")
}
}
僅僅為了練習(xí)癞志,生產(chǎn)環(huán)境還是不推薦使用往枷,感覺太重了。
最近看了一下Go 1.14
的提案凄杯,關(guān)于try
關(guān)鍵字的引入, try參考错洁。按其所展示的功能,如果自己實現(xiàn)的話戒突,應(yīng)該會用到反射功能屯碴。那么對于現(xiàn)在如此依賴 error
檢查的函數(shù)實現(xiàn)來說,是否合適,挺懷疑的膊存,等Go 1.14
出了窿锉,驗證一下。
3 小結(jié)
反射的最佳應(yīng)用場景是程序的啟動階段膝舅,實現(xiàn)一些類型檢查嗡载、注冊等前置工作,既不影響程序性能同時又增加了代碼的可讀性仍稀。最近迷上新褲子洼滚,所以別再問我什么是反射了:)