做者:Jon Bodner | 地址:Learning to Use Go Reflection — Part 2html
這篇博文介紹的內容比較實在,主要是關於兩方面的內容。一是介紹 reflection 在 encoding/json 中的應用,另外一個是利用反射開發了一個 Cacher 工廠函數,實現函數式編程中的記憶功能,其實就是根據輸入對輸出進行必定限期的緩存。git
這篇文章的翻譯沒有上一篇那麼輕鬆,由於涉及了一些函數式編程的術語,以前也並無接觸過。爲了翻譯這篇文章,簡單閱讀了網上的一篇關於函數式編程的文章,文章地址。望沒有知識性錯誤。github
譯文以下:golang
上一篇文章,(閱讀英文原版),咱們介紹了 Go 的反射包 reflection。並經過一些示例介紹了它的特性。可是,咱們還不清楚它究竟有什麼。數據庫
經過反射實現的功能,不用反射也能實現,並且更加高效簡潔。可是 Go 團隊確定不會由於本身而爲 Go 增長一個新的特性。編程
那究竟什麼狀況下會使用反射呢?json
經過反射,咱們能夠實現各類奇淫巧技。但天天的工做中,我該如何使用它呢?api
其實,大部分時間裏,咱們都用不到它。反射主要是用在一些特殊的場景下,使一些不可能的實現成爲可能。咱們常會在一些庫、工具、框架中找到反射的使用場景。緩存
那你是否能夠告訴我,哪些庫、框架或工具中使用反射呢?一個技巧,查看函數參數類型。若是一個函數的參數類型是 interface{},那麼,它極有可能使用了反射來檢查或改變參數的值。bash
反射,最多見的使用場景之一,是對網絡或文件中的數據進行解包和組包。當你經過 struct tag 映射 JSON 或數據庫中的數據時,即是經過反射實現的。這類場景,咱們一般會用某個庫幫助咱們建立結構體實例,它經過分析 struct tag 和數據,以此爲 struct 的字段賦值。
咱們就以 Go 官方標準庫中的 JSON 解包爲例,來介紹一下它的實現。
經過調用 json.Unmarshal 函數,咱們能夠把 JSON 字符串解包並賦值給某個變量。Unmarshal 函數接收兩個參數:
深刻看看這個函數到底是如何進行反射的?
閱讀 json 包的源碼,其中有個私有函數 unmarshal,主要看其中與反射相關的部分代碼以下:
func (d *decodeState) unmarshal(v interface{}) (err error) {
<skip some setup>
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Ptr || rv.IsNil() {
return &InvalidUnmarshalError{reflect.TypeOf(v)}
}
d.scan.reset()
// We decode rv not rv.Elem because the Unmarshaler interface
// test must be applied at the top level of the value.
// 傳的是 rv,而不是 rv.Elem,由於結果傳遞給最頂層的 value
d.value(rv)
return d.savedError
}
複製代碼
上面的代碼中,首先會經過反射驗證變量類型,是不是指針類型,若是是,將變量 v 的 reflect.Value 傳給 value 方法。
在 value 方法中,首先檢查 JSON 字符串表示的類型,array、object、仍是字面量。不一樣的類型將由不一樣方法處理。舉例來講,若是解析 JSON Object。
將會有不少地方用到反射。
好比,使用反射檢查 v 是不是 nil interface。
// Decoding into nil interface? Switch to non-reflect code.
if v.Kind() == reflect.Interface && v.NumMethod() == 0 {
v.Set(reflect.ValueOf(d.objectInterface()))
return
}
複製代碼
若是是把 JSON object 賦值給 map。
switch v.Kind() {
case reflect.Map:
// Map key must either have string kind, have an integer kind,
// or be an encoding.TextUnmarshaler.
t := v.Type()
switch t.Key().Kind() {
case reflect.String,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
default:
if !reflect.PtrTo(t.Key()).Implements(textUnmarshalerType) {
d.saveError(&UnmarshalTypeError{Value: "object", Type: v.Type(), Offset: int64(d.off)})
d.off --
d.next() // skip over { } in input
return
}
}
if v.IsNil() {
v.Set(reflect.MakeMap(t))
}
case reflect.Struct:
// ok
default:
d.saveError(&UnmarshalTypeError{Value: "object", Type: v.Type(), Offset: int64(d.off)})
d.off --
d.next() // skip over { } in input
return
}
複製代碼
若是是把 JSON object 賦值給 struct。
subv = v
destring = f.quoted
for _, i := range f.index {
if subv.Kind() == reflect.Ptr {
if subv.IsNil() {
subv.Set(reflect.New(subv.Type().Elem()))
}
subv = subv.Elem()
}
subv = subv.Field(i)
}
複製代碼
以上只是一個簡單的演示,主要是關於,JSON 包中反射是如何使用的。若是但願本身閱讀代碼,源碼在 encoding/json/decode.go。
Json Unmarshal 是反射的其中一個使用案例。還有其餘場景嗎?接下來,咱們將用反射開發一個庫,基於 "記憶法" 實現短時間緩存。
或許你並不熟悉這個術語,"記憶" 出自於函數式編程。函數式編程中傾向於強制推行某些規則,好比,參數和變量一般是不可修改的,建立以後便不可變。函數式編程嘗試限制編程的 "反作用"。
一個實際的程序基本是不可能的沒有 "反作用" 的,由於總會涉及諸如信息打印、寫文件、向數據庫插入數據等事情。但其餘的一些 "反作用",好比更新全局變量,將會致使程序很難追蹤。函數式編程的一個目標,讓程序中的數據追蹤變得簡單,咱們能夠很是容易地就明白程序在作什麼?
函數式編程還有一些其餘好處。好比,當一個函數輸入和返回不變且沒有 "反作用" 時,每次調用函數,相同的輸入,作一樣的工做,返回相同的結果。若是咱們保存了這些結果,重複的工做就沒有必要作第二次了。
到此,咱們開始引入 "記憶" 的概念。它相似於函數級別的緩存。"記憶" 是經過在恆定的函數上包裹函數,實現輸入輸出的緩存,從而避免重複沒必要要的工做。但一個函數被 "記憶",對於一個輸入,只有執行一次工做。相同的輸入執行第二次函數調用時,返回值將從緩存中獲取,而不是從新計算一次。對於那些複雜或很是慢的操做,如此對性能的提高將是很是大的。
Go 可能不算是函數式編程語言,但咱們依然能夠經過它踐行本身的想法。這種編程風格稍微有點嚴格,可是它避免了輸入輸出的更新,最小化了程序的 "反作用",使你的程序很是易讀。
咱們將實現一小短期的緩存,而不是永遠。在微服務架構中,這是一種更加通用的模式。好比以下的場景:
一個服務提供數據,另外一個服務獲取數據。由於數據是經過網絡傳遞的,會佔用必定時間。這將會下降系統的總體性能。若是數據不是常常改變的且數據延遲幾秒更新也沒有關係,那麼能夠暫時緩存這個數據,它將給你的系統帶來一個顯著的性能提高。經過函數式編程的 "記憶",咱們能夠在也沒有對太多 API 的修改實現緩存,避免系統額外的網絡調用。
如何實現?咱們將經過反射實現三件事:
還有一個限制,全部的輸入參數必須可比較,在 Go 中,可比較的類型及能夠經過 == 符號進行布爾運算。咱們將用 Go 中的映射 map 關聯輸入和輸出,Go 中 map 的 key 必須是可比較的,這頗有意義,由於要確認輸入參數是否出現過,咱們須要檢查先後的相等性。
幸運的是,Go 中只有四種狀況是不可比較的,以下:
讓咱們開始定義 Cacher 函數,相似以下的形式:
// Takes in a function and a time.Duration and returns a caching version of the function
// 接收兩個參數,分別是函數和時間,返回的是一個緩存版本的函數
//
// The limitations on memoization are:
// 限制以下:
//
// — there must be at least one in parameter
// - 必須有一個輸入參數
//
// — there must be at least one out parameter
// - 必須有一個輸出參數
//
// — the in parameters must be comparable. That means they cannot be of kind slice, map, or func,
// nor can input parameters be structs that contain (at any level) slices, maps, or funcs.
// - 輸入參數必須是可比較的,
//
// Be aware that if your memoized function has any side-effects (does anything that isn’t
// reflected in the output, like print to the screen or write to a database) the side-effects
// will be performed by the function only the first time that the function is invoked with
// particular set of values.
// 要明白瞭解,若是被記憶的函數有任何的反作用(指任何不能在返回結果中反應出來的東西,好比打印、寫數據庫),這些 "反作用"
// 將只會執行一次。
func Cacher(f interface{}, expiration time.Duration) (interface{}, error) {
return f, nil
}
複製代碼
代碼並無作什麼,但經過它,咱們明白了接下來的目標。下面,咱們正式開始填寫代碼,首先經過反射檢查,咱們傳遞的確實是一個函數。
func Cacher(f interface{}, expiration time.Duration) (interface{}, error) {
ft := reflect.TypeOf(f)
if ft.Kind() != reflect.Func {
return nil, errors.New("Only for functions")
}
return f, nil
}
複製代碼
接下來,建立一個結構體用來存放咱們的輸入參數。在建立結構體時,咱們須要保證必須有一個輸入和一個輸出,而且全部的輸入都是可比較的。
unc buildInStruct(ft reflect.Type) (reflect.Type, error) {
if ft.NumIn() == 0 {
return nil, errors.New("Must have at least one param")
}
var sf []reflect.StructField
for i := 0; i < ft.NumIn(); i++ {
ct := ft.In(i)
if !ct.Comparable() {
return nil, fmt.Errorf("parameter %d of type %s and kind %v is not comparable", i+1, ct.Name(), ct.Kind())
}
sf = append(sf, reflect.StructField{
Name: fmt.Sprintf("F%d", i),
Type: ct,
})
}
s := reflect.StructOf(sf)
return s, nil
}
func Cacher(f interface{}, expiration time.Duration) (interface{}, error) {
ft := reflect.TypeOf(f)
if ft.Kind() != reflect.Func {
return nil, errors.New("Only for functions")
}
inType, err := buildInStruct(ft)
if err != nil {
return nil, err
}
if ft.NumOut() == 0 {
return nil, errors.New("Must have at least one returned value")
}
fmt.Println("inType looks like", inType)
return f, nil
}
複製代碼
接下來,還剩最後一步,定義一個 map 變量,用它存放輸入輸出的緩存,而且用 reflection 反射在 f 函數基礎上生成具備緩存能力的新函數。
type outExp struct {
out []reflect.Value
expiry time.Time
}
func Cacher(f interface{}, expiration time.Duration) (interface{}, error) {
ft := reflect.TypeOf(f)
if ft.Kind() != reflect.Func {
return nil, errors.New("Only for functions")
}
inType, err := buildInStruct(ft)
if err != nil {
return nil, err
}
if ft.NumOut() == 0 {
return nil, errors.New("Must have at least one returned value")
}
m := map[interface{}]outExp{}
fv := reflect.ValueOf(f)
cacher := reflect.MakeFunc(ft, func(args []reflect.Value) []reflect.Value {
iv := reflect.New(inType).Elem()
for k, v := range args {
iv.Field(k).Set(v)
}
ivv := iv.Interface()
ov, ok := m[ivv]
now := time.Now()
if !ok || ov.expiry.Before(now) {
ov.out = fv.Call(args)
ov.expiry = now.Add(expiration)
m[ivv] = ov
}
return ov.out
})
return cacher.Interface(), nil
}
複製代碼
完成!
再來看一下上面的代碼,首先,咱們定義了一個結構體 outExp,用它存放輸出和過時時間。
接着,咱們定義了一個 map,它的 key 是interface{},值是 outExp 類型。它們的選擇都是有緣由的。先說 key 是 interface{} 類型的緣由。以前的例子,咱們經過反射建立了一個結構體,這種結構體沒有名稱,爲了存儲實例,咱們不得不使用 interface{} 類型表示。關於返回類型,當你用反射調用函數,它的返回類型是 []reflect.Value。一樣,傳遞給 MakeFunc 的閉包函數返回的也是這種類型的值。 爲了不值拷貝,咱們經過 []reflect.Value 保存返回值,並把它存入 map。
在閉包中,咱們經過反射構造了一個自定義類型的實例,將傳給函數的參數放入其中。接着,檢查 m 中是否存在實例等於它,若是沒有,或已通過期,咱們將調用包裹函數,而後將響應結果和過時時間保存進變量 ov 中。接着,以自定義結構體的實例爲 key,將 ov 保存進 m 中。最後,返回 ov.out 便可。
到此,咱們正式完成了 Cacher 工廠函數,它能夠包裹 Go 中幾乎全部的函數,實現必定期限的緩存。
咱們如何使用呢?一個例子,以下:
func AddSlowly(a, b int) int {
time.Sleep(100 * time.Millisecond)
return a + b
}
func main() {
ch, err := Cacher(AddSlowly, 2*time.Second)
if err != nil {
panic(err)
}
chAddSlowly := ch.(func(int, int) int)
for i := 0; i < 5; i++ {
start := time.Now()
result := chAddSlowly(1, 2)
end := time.Now()
fmt.Println("got result", result, "in", end.Sub(start))
}
time.Sleep(3 * time.Second)
start := time.Now()
result := chAddSlowly(1, 2)
end := time.Now()
fmt.Println("got result", result, "in", end.Sub(start))
}
複製代碼
例子中,僅僅是 sleep 100ms,而後,求兩個數的和。實際的狀況多是,數據庫的查詢或網絡服務的調用。因爲 Go 沒有泛型,Cacher 返回的函數須要轉化爲合適的類型,錯誤檢查也要求有幾行代碼,interface{} 的 ch 也須要轉化爲實際的函數類型。
執行代碼,將會獲得以下的輸出:
$ go run cacher.go
got result 3 in 100.079405ms
got result 3 in 3.873µs
got result 3 in 561ns
got result 3 in 462ns
got result 3 in 398ns
got result 3 in 100.054602ms
複製代碼
第一次執行佔用了大概 100 ms(計算處理時間),接下里的幾回調用都只用了幾百納秒。在暫停了 3 秒後,執行最後一次,再次耗時 100 ms。
再說最後一點,反射對性能有必定的影響。若是執行的是密集性運算,或是調用網絡服務,經過反射在上面加一層代碼,這對性能將不會有太大的影響。可是,多數代碼都是很是快的。極有可能,你代碼中的大部分方法的執行時間都是幾百毫秒之內,此類場景,就要當心反射的使用了,此時,反射會性能有較大影響,咱們須要從新思考是否值得。
總而言之,當咱們再遇到各類類型的問題時,能夠回想下反射提供的可能。當遇到看起不可能解決的問題時,好比雖然兩個類型的處理邏輯類似,可是類型的自身的共性很少,這時候,反射將成爲你的祕密武器。