這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
一般提到回調,第一反映就是函數回調,太熟悉了。在學習golang的過程中,通過閱讀相關原始碼,發現golang結合channel和WaitGroup,會有非常特殊的有別於函數回調的結果返回方式,常用於相對耗時運算的結果擷取。其核心思路就是利用延時訊號通知,來返回。因為暫時沒查到中文的定義,暫時叫做Call回調,或者叫完成通知模式。
通道組合
Call就是一個公用的可訪問的結構體定義,用於封裝使用者請求與結果,然後通過內部額外加入的channel封裝實現非同步結果的返回。對使用者而言,需要知道Call的channel屬性,以及架構定義的非同步呼叫方法。
//Call定義type Call struct { Request interface{} Reply interface{} Done chan *Call //用於結果返回時,訊息通知,使用者必須依靠這個來擷取真正的結果。 }//...//具體使用的時候,通過架構或平台提供的方法先擷取到call執行個體//方法定義通常為:foo(req ,reply interface{},done chan *Call)*callcall:=foo(req,reply,nil)//架構定義的非同步呼叫方法,最後一個參數為chan,能夠為nil//do somethingreply<-call.Done//此處阻塞,等待foo的執行結果//do another thing
完整範例程式碼可以查看go語言基本的net\rpc\client.go檔案。由於代碼中業務比較多,這裡給出一個應用的代碼抽象:
package mainimport ( "fmt" "time")// Call的基本定義,對外部使用者的請求、返回以及非同步使用進行封裝。type Call struct { Request interface{} Reply interface{} Done chan *Call //用於結果返回時,指向自己的指標}// 非常重要的非同步呼叫結果返回,供架構內部使用。func (call *Call) done() { select { case call.Done <- call: // ok default: // 阻塞情況處理,這裡忽略 }}func main(){ for i:=0;i<100;i++{ var reply *int call:=GO(i,reply,nil) //擷取到了call,但此時call.Reply還不是運算結果 //先列印結果還沒有計算出來的情況 fmt.Printf("i=%d,運算前:call.Reply=%v \n",i,call.Reply.(*int)) result:=<-call.Done //等待Done的通知,此時call.Reply發生了變化。 fmt.Printf("i=%d,運算後:call.Reply=%v,result=%+v \n",i,*(call.Reply.(*int)),*(result.Reply.(*int))) }}// 供業務調用的非同步計算函數封裝,使用者只需要瞭解對應參數。func GO(req int,reply *int,done chan *Call)*Call{ if done==nil{ done=make(chan *Call,10) }else{ if cap(done)==0{ fmt.Println("chan容量為0,無法返回結果,退出此次計算!") return nil } } call:=&Call{ Request:req, Reply:reply, Done:done, } //調用一個可能比較耗時的計算,注意用"go" go caculate(call) return call}//真正的業務處理代碼//簡單示意,其實存在讀寫競爭。run -race 就會出現提示func caculate(call *Call){ //假定運算一次需要耗時1秒 time.Sleep(time.Second) tmp:=call.Request.(int)*5 call.Reply=&tmp call.done()}
由此可以看出,適合於運算需要一定時間,但可以在等待過程中,處理其他業務的情況。
wg組合
前面提到了Call封裝通道,來實現非同步。那麼,對於耗時的操作能否避免相同多次操作(方法相同、輸入參數相同、結果肯定相同)的重複性呢?這時候,可以考慮採用封裝WaitGroup來實現。
在github上,golang的groupcache有一個這樣的模組封裝,可以直接拿來使用,這裡copy如下:
// call is an in-flight or completed Do calltype call struct { wg sync.WaitGroup val interface{} err error}// Group represents a class of work and forms a namespace in which// units of work can be executed with duplicate suppression.type Group struct { mu sync.Mutex // protects m m map[string]*call // lazily initialized,如果要通用,此處string->inteface{}}// Do executes and returns the results of the given function, making// sure that only one execution is in-flight for a given key at a// time. If a duplicate comes in, the duplicate caller waits for the// original to complete and receives the same results.func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) { g.mu.Lock() // 效能要高點,可以建立時初始化 if g.m == nil { g.m = make(map[string]*call) } // 輸入參數是否已經存在,存在就等待對應結果,不用計算。 if c, ok := g.m[key]; ok { g.mu.Unlock() c.wg.Wait() return c.val, c.err } // 不存在,則開始執行fn c := new(call) c.wg.Add(1) g.m[key] = c g.mu.Unlock() //擷取到計算結果 c.val, c.err = fn() c.wg.Done() g.mu.Lock() delete(g.m, key) g.mu.Unlock() return c.val, c.err}
來嘗試調用一下:
func main() { req := "執行請求" g := new(Group) // 藉助之前文章提及的waitgroup封裝函數,不用也行,運行時加入 '-race' var wg waitgroup.WaitGroupWrapper for i := 0; i < 1000; i++ { wg.Wrap(func() { j, _ := g.Do(req, NeedSecondTime) fmt.Println("NeedSecondTime被調用了=j", j) }) } wg.Wait()}// 每執行一次,都需要1秒var counter=0func NeedSecondTime()(interface{},error){ time.Sleep(time.Second) counter++ return counter,nil}
毫無疑問,結果是1,NeedSecondTime只執行了一次。