這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
go語言中,作為一等類型的函數,是可以作為值來傳遞和使用。而閉包,則是函數和環境變數的結合。將函數作為參數,利用閉包的特性,可以用簡潔的代碼提供實用的功能。
之前提到call通過wg組合,來規避同一時刻同樣的耗時操作導致系統崩潰。【golang積累-Call回調模式】,這個在Groupcache【github】的代碼中用於同樣資料在惰性載入的時候,對資料庫的過熱請求。具體代碼參見:【singleflight.go】。
但如果不是同一時刻的存取違規,而是one by one一次一次的重複處理,這時我們通常為了避免重複計算,尤其是耗時且又通用的計算處理、資料庫查詢,就會考慮cache。通常會非常親切的在很多地方類似的代碼:
cache:=make(map[string]interface{})//...if _,founded:=cache[key];founded{//do something}else{v:=function(key)cache[key]=v}
如果業務中使用情境較多,可考慮封裝到高階函數中,在函數內部封裝cache進行過濾。
memcache函數的基本形式
//需要被cache結果的函數type memoizeFunction func(int, ...int) int//封裝cache的高階函數,每次運算都會先尋找cache,如果沒有則計算func Memoize(function memoizeFunction)memoizeFunction{ //封裝了的cache cache:=make(map[string]int) return func(x int,xs ...int)int{ //1、將函數的輸入參數展開併合並為字串,作為cache的key。對於參數按順序的情況非常實用。 key:=fmt.Sprint(x) for _,i:=range xs{ key+=fmt.Sprintf(",%d",i) } //2、在cache中尋找 if value,found:=cache[key];found{ return value } //3、沒有緩衝,則計算,並將結果那入到cache中 value:=function(x,xs...) cache[key]=value return value }}//具體的業務方法非常耗時的計算var caculate = Memoize(func(x int, xs ...int) int { //通過sleep類比耗時1秒的內部處理 time.Sleep(time.Second) //隨機返回一個結果 return rand.Intn(10)})func main() { //類比計算100次,實際只有前10次是真實計算,後邊都會cache出結果。 for i := 0; i < 100; i++ { caculate(i % 10) }}
代碼中,Memoize這個函數,其實有類似於result pool的作用。 每次只需要修改caculate內部的具體代碼即可。其是否已被cache還是重新計算都被Memoize進行了封裝。對於這部分代碼,可以理解為:
- memoize就是一個獨立的運列區域,
- caculate通過Memoize的傳回值定位,訪問只由它可見的cache,可以把第19行代碼轉換為匿名函數,就很清晰:
//...//3、沒有緩衝,則計算,並將結果那入到cache中 value:=func(x,xs...int)int{//<------此處開始,用匿名函數展開caculate的函數代碼 time.Sleep(time.Second) return rand.Intn(10) }(x,xs...) cache[key]=value //<--------對匿名函數而言,cache是外部公用的 return value
memcache函數的擴充
現實中,前面代碼還有幾個缺陷:
1. 傳回值是整形,限制很大。
2. 輸入值是整形,不適合其他形式。
由於go的文法特徵,對傳回值可以改為interface{},根據具體業務再進行轉換。而輸入值,則根據情況斟酌是否轉換。
在有些資料中,提到斐波拉契函數的計算最佳化,就用到了cache來規避多次遞迴。代碼如下:
package mainimport "fmt"// 將結果形式擴充為interface{}type memoizeFunction func(int, ...int) interface{}var Fibonacci memoizeFunctionfunc init() { Fibonacci = Memoize(func(x int, xs ...int) interface{} { if x < 2 { return x } return Fibonacci(x-1).(int) + Fibonacci(x-2).(int) })}func Memoize(function memoizeFunction) memoizeFunction { //封裝了的cache cache := make(map[string]interface{}) return func(x int, xs ...int) interface{} { key := fmt.Sprint(x) for _, i := range xs { key += fmt.Sprintf(",%d", i) } if value, found := cache[key]; found { return value } //沒有緩衝,則計算,並將結果那入到cache中 value := function(x, xs...) cache[key] = value return value }}func main() { fmt.Println("Fibonacci(45)=", Fibonacci(45))}
此時,由於用到了遞迴,感覺會複雜一些。原理其實沒變,
Memoize中的cache是在Fibonacci初始化的時候就已經建立好了,也就是下邊這行代碼出現的時候:
Fibonacci = Memoize(func(x int, xs …int) interface{} {
Fibonacci遞迴的時候,僅僅就是從key := fmt.Sprint(x)開始執行,這與傳統的遞迴調用是相通的。
其他
代碼中的key轉換,實際上是有序的
key:=fmt.Sprint(x)for _,i:=range xs{ key+=fmt.Sprintf(",%d",i)}
如果入參是無序集合,而集合元素的順序確不同,此時key並不相同,無法直接定位結果,依然會重新計算。考慮將入參改為interface{},並進行排序處理應該可以最佳化。
cache是函數內部私人
如果同一個檔案中,即使多個函數使用Memoize也不用擔心相同key的衝突,因為每個函數都會有一個內部的cache,這是閉包函數的特點。
簡言之,通過封裝cache的記憶閉包,結合call模式,將會大幅提升系統的效能和健壯性。本質上,就是利用文法替代了一些模式上的應用。當然,從代碼風格上,個人認為golang的記憶閉包,比scala的高階函數要難理解。這或許算是個例吧!