Go 編程:圖解反射

原文發佈在個人我的站點: Go 編程:圖解反射git

反射三原則太難理解,看一張圖你就懂了。完美解釋兩個關鍵詞 interface value 與 reflection object 是什麼。github

1. 圖解反射

在使用反射以前,此文The Laws of Reflection必讀。網上中文翻譯版本很多,能夠搜索閱讀。golang

開始具體篇幅以前,先看一下反射三原則:編程

  • 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。有點難理解,畫張圖可能你就懂了。app

先看一下什麼是反射對象 reflection object? 反射對象有不少,可是其中最關鍵的兩個反射對象reflection object是:reflect.Typereflect.Value.直白一點,就是對變量類型的抽象定義類,也能夠說是變量的元信息的類定義.框架

再來,爲何是接口變量值 interface value, 不是變量值 variable value 或是對象值 object value 呢?由於後二者均不具有普遍性。在 Go 語言中,空接口 interface{}是能夠做爲一切類型值的通用類型使用。因此這裏的接口值 interface value 能夠理解爲空接口變量值 interface{} valueide

結合圖示,將反射三原則概括成一句話:函數

經過反射能夠實現反射對象 reflection object接口變量值 interface value之間的相互推導與轉化, 若是經過反射修改對象變量的值,前提是對象變量自己是可修改的。性能

2. 反射的應用

在程序開發中是否須要使用反射功能,判斷標準很簡單,便是否須要用到變量的類型信息。這點不難判斷,如何合理的使用反射纔是難點。由於,反射不一樣於普通的功能函數,它對程序的性能是有損耗的,須要儘可能避免在高頻操做中使用反射。ui

舉幾個反射應用的場景例子:

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 小結

反射的最佳應用場景是程序的啓動階段,實現一些類型檢查、註冊等前置工做,既不影響程序性能同時又增長了代碼的可讀性。最近迷上新褲子,因此別再問我什麼是反射了:)

參考資源:

相關文章
相關標籤/搜索