Go 譯文之如何使用反射 Part 2

做者:Jon Bodner | 地址:Learning to Use Go Reflection — Part 2html

譯者前言

這篇博文介紹的內容比較實在,主要是關於兩方面的內容。一是介紹 reflection 在 encoding/json 中的應用,另外一個是利用反射開發了一個 Cacher 工廠函數,實現函數式編程中的記憶功能,其實就是根據輸入對輸出進行必定限期的緩存。git

這篇文章的翻譯沒有上一篇那麼輕鬆,由於涉及了一些函數式編程的術語,以前也並無接觸過。爲了翻譯這篇文章,簡單閱讀了網上的一篇關於函數式編程的文章,文章地址。望沒有知識性錯誤。github

譯文以下:golang


上一篇文章,(閱讀英文原版),咱們介紹了 Go 的反射包 reflection。並經過一些示例介紹了它的特性。可是,咱們還不清楚它究竟有什麼。數據庫

經過反射實現的功能,不用反射也能實現,並且更加高效簡潔。可是 Go 團隊確定不會由於本身而爲 Go 增長一個新的特性。編程

那究竟什麼狀況下會使用反射呢?json

尋找反射使用案例

經過反射,咱們能夠實現各類奇淫巧技。但天天的工做中,我該如何使用它呢?api

其實,大部分時間裏,咱們都用不到它。反射主要是用在一些特殊的場景下,使一些不可能的實現成爲可能。咱們常會在一些庫、工具、框架中找到反射的使用場景。緩存

那你是否能夠告訴我,哪些庫、框架或工具中使用反射呢?一個技巧,查看函數參數類型。若是一個函數的參數類型是 interface{},那麼,它極有可能使用了反射來檢查或改變參數的值。bash

JSON 處理

反射,最多見的使用場景之一,是對網絡或文件中的數據進行解包和組包。當你經過 struct tag 映射 JSON 或數據庫中的數據時,即是經過反射實現的。這類場景,咱們一般會用某個庫幫助咱們建立結構體實例,它經過分析 struct tag 和數據,以此爲 struct 的字段賦值。

咱們就以 Go 官方標準庫中的 JSON 解包爲例,來介紹一下它的實現。

經過調用 json.Unmarshal 函數,咱們能夠把 JSON 字符串解包並賦值給某個變量。Unmarshal 函數接收兩個參數:

  • 類型爲 []byte 的 JSON 字符串;
  • 類型爲 interface{},用於存放 JSON 解析結果的變量;

深刻看看這個函數到底是如何進行反射的?

閱讀 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 中只有四種狀況是不可比較的,以下:

  • 切片,Slices;
  • 映射,Maps;
  • 函數,Functions;
  • 結構體,成員包含 Slice、Map 或 Function;

讓咱們開始定義 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。

運行示例

新的祕密武器

再說最後一點,反射對性能有必定的影響。若是執行的是密集性運算,或是調用網絡服務,經過反射在上面加一層代碼,這對性能將不會有太大的影響。可是,多數代碼都是很是快的。極有可能,你代碼中的大部分方法的執行時間都是幾百毫秒之內,此類場景,就要當心反射的使用了,此時,反射會性能有較大影響,咱們須要從新思考是否值得。

總而言之,當咱們再遇到各類類型的問題時,能夠回想下反射提供的可能。當遇到看起不可能解決的問題時,好比雖然兩個類型的處理邏輯類似,可是類型的自身的共性很少,這時候,反射將成爲你的祕密武器。


微信公衆號
相關文章
相關標籤/搜索