6月2日Go語言中文網在杭州舉辦了線下的MeetUp活動,這次活動辦很成功,感謝站長polaris在杭州舉辦活動的提議,感謝Seekload的籌備與主持,感謝Aaron提供場地,感謝所有到場者的技術經驗分享,沒有你們就沒有這次精彩的活動。
在活動上,我做了個主題分享,今天把分享整理成文章,分享給學習Go語言的各位朋友。
參加本次活動的朋友,大多是剛接觸Go,少數幾個朋友把玩Go 2~3年了,所以我把主題定位到能讓所有人聽懂的主題。另外,大家所處行業各有不同,這就要求專注介紹Go本身的特性,這才是大家通用的地方。
最後選題為First class function in Go,這次沒有做中文翻譯,避免翻譯後有誤解。這個特性淺顯易懂,但掌握Go語言的思維,才能把它用好。
線下分享後,證明選題選對了,大家都能聽懂,所以現在不了解First class function的朋友不用著急,後面我會層層推進的方式介紹,相信你一定能理解,那就進入正文吧。
目錄優先奉上
First class function in Go概念介紹定義函數類型聲明函數類型的變量和為變量賦值高階函數函數作為其他函數入參函數作為返回值+動態創建匿名函數閉包Demo場景介紹版本1版本2版本3總結源碼PPT下載雲象介紹活動總結
First class function in Go概念介紹幻燈片04某個程式語言擁有First class function特性指可以把函數作為變量對待。也就說,函數與變量沒有差別,它們是一樣的,變量出現的地方都可以替換成函數,並且編譯也是可以通過的,沒有任何語法問題。
在Go裡,變量可以存在於哪些地方?
幻燈片05變量可以被聲明、定義,可以使用type創建變量的類型,可以作為函數的入參和返回值,可以存在slice, array, map等數據結構裡,可以被動態的創建。
在Go中,函數也可以被聲明、定義,可以使用type創建一個函數類型,可以作為其他函數的入參和返回值,可以保存在其他類型的數據結構裡,最後,函數是可以被動態創建的。
簡要歸類一下就是下圖的樣子,除了上面提到的內容,還有匿名函數和閉包,將按下圖順序介紹每一個小特性。
幻燈片06定義函數類型幻燈片07使用type定義一個函數類型,type後是類型名稱,本例中是Operation,再後面是類型的定義,對於函數而言,被稱為signature,即函數籤名,這個函數籤名表示:Operation類型的函數,它以2個int類型為入參,以1個int為返回值。所有滿足該函數籤名的函數,都是Operation類型的函數。
幻燈片08函數Add和Sub都符合Operation的籤名,所以Add和Sub都是Operation類型。
聲明函數類型的變量和為變量賦值幻燈片09變量op是Operation類型的,可以把Add作為值賦值給變量op,執行op等價於執行Add。
高階函數高階函數分為函數作為入參和函數作為返回值2部分。
函數作為其他函數入參幻燈片10定義一個Calculator結構體,它始終保持計算後的結果。
它有一個方法Do,入參為一個Operation類型的函數op和1個int類型的變量a,使用計算器的值c.v和a作為op的入參,進行指定運算,並把結果保存會c.v。
main中,聲明了一個變量calc,calc.v初始值為0,然後運行了加1和減2的操作,加減法的完成使用的我們之前定義的函數Add和Sub。操作等價於:
1calc.v = Add(calc.v, 1)
2calc.v = Sub(calc.v, 2)
這次,改變Operation的定義,修改為接收1個int類型的入參,返回1個int類型的返回值。
同時修改函數Add和Sub,它們接收1個int類型的入參,返回1個Operation類型的函數,這個函數是動態創建出來的。
以Add為例介紹,在Add裡動態創建了一個函數,
1func(a int) int {
2 return a + b
3}
該函數實現了在變量a基礎上加b的操作,並返回結果,我們把這個函數賦值給變量addB,把addB作為返回值返回。
所以本例實現了以函數作為返回值和動態創建函數。
幻燈片12Operation,Add和Sub修改後,Calculator也要同步修改,方法Do修改為只接收Operation類型的函數。
main函數裡,注意Do的入參:Add(1),它實現的效果是,創建了1個函數,該函數接收1個值,然後把這個值+1返回,如果用數學表示就是這樣:
1
2add1(x) = x + 1
同理,Sub(2)的數學表示如下:
1
2sub2(x) = x - 2
所以2次Do操作等價於:
1
2calc.v = add1(calc.v)
3
4calc.v = sub2(calc.v)
上圖左邊是普通函數,func後為函數名,然後為函數籤名。右邊只有func和函數籤名,缺少函數名,右邊的情況為匿名函數。
幻燈片14以Add函數其中定義的函數為例:
1func(a int) int {
2 return a + b
3}
這就是1個匿名函數,它沒有名字。addB並不是函數的名字,只是1個變量名而已,只不過這個變量名的類型是沒有顯示定義出來的。
Add通常簡寫為右邊的形式。
閉包幻燈片15很多人搞不清什麼是匿名函數,什麼是閉包,所以這裡分開介紹這2個概念。
閉包指有權訪問另一個函數作用域中的變量的函數。大白話就是,可以創建1個函數,它可以訪問其他函數遍歷,但不需要傳值。
仍然是Add函數為例,比如匿名函數裡直接使用了變量b,該匿名函數也是閉包函數。
閉包的特性註定了,閉包函數要定義在一個函數裡面,定義在一個函數裡面又只能是匿名函數。
那,匿名函數和閉包是不是就等價了?
No,一個函數可以是匿名函數,但不是閉包函數,因為閉包有時是有副作用的。
幻燈片16我們想並發的把sl中的值列印出來,結果為何會是右邊這樣?
因為並發的匿名函數,使用的是test1中的i,v,即這是閉包函數,所有的goroutine都共享這2個值,並且啟動1個goroutine後,這2個值變為下一個位置的值。你運行的結果也許不是9 9 9….,因為這個goroutine的調度有關。
如何才能符合預期的列印?只使用匿名函數進行傳值,不使用閉包。
幻燈片17Demo接下來以一個實際的場景,和3種實現版本看如何用Go的思維去解決問題。
場景介紹幻燈片19做Go語言工作,尤其是跟網絡打交道的工作,連接管理是逃不開的。我做區塊鏈相關的技術工作,區塊鏈中也有網絡管理,所以我就以區塊鏈的網絡管理為場景進行介紹,但不涉及具體的技術細節,大家莫慌,只需要理解2個概念就行。
區塊鏈是構建在P2P網絡之上,在P2P網絡中:
一個節點即可以是伺服器也可以是客戶端,被稱為Host,
和本節點連接的所有節點都被稱為Peer。
具體的場景是:Host需要保存所有建立連接的Peer,並對這些Peer進行維護:增加和刪除Peer,並且提供Peer的查詢和向所有Peer廣播消息的接口。
針對這個問題場景,我寫了3個版本的Demo,我們依次來介紹,再看的時候,可以思考其中的不同。
版本1幻燈片20先看Peer定義,Peer中保存了ID,我們可以通過ID來表示全網中所有的節點,Peer中還有其他欄位,比如網絡連接、地址、協議版本等信息,此處已經省略掉。
Peer有一個WriteMsg的方法,實現向該Peer發送消息的功能,例子中使用列印替代。
Peer的定義在3個版本中都不會發生變化,所以後面就不再展示。
幻燈片21Host通過peers保存了所有連接的Peer,可以通過Peer.ID對Peer進行索引。Peer的管理是並發場景,比如,我們可能同時接收到多個Peer的連接,又同時需要向所有Peer廣播消息,需要對peers加鎖保護。最後,我們省略了Host的其他欄位。
NewHost()用來創建一個Host對象,用來代表當前節點。
友情提醒:Host在每一個版本都會不同。
幻燈片22Host有4個方法,分別是:
AddPeer: 增加1個Peer。
RemovePeer: 刪除1個Peer。
GetPeer: 通過Peer.ID查詢1個Peer。
BroadcastMsg: 向所有Peer發送消息。
每一個方法都需要獲取lock,然後訪問peers,如果只讀取peers則使用讀鎖。
第1個版本已經介紹完了,大家可以思考一下版本1的缺點。
幻燈片23第1個版本跟其他語言實現其實沒有本質區別,用C++、Java等也能寫出上面邏輯的代碼,只不過這個是Go語言實現的罷了。
這個版本是一個communicate by sharing memory的體現,具體來講,每個goroutine都是1個實體,它們同時運行,調用Host的不同方法來訪問peers,只有拿到當前lock的goroutine才能訪問peers,仿佛當前goroutine在同其他goroutine講:我現在有訪問權,你們等一下。本質上就是,通過共享Host.lock這塊內存,各goroutine進行交流(表明自己擁有訪問權)。
版本2幻燈片24很多Go老手都聽過這句話了,這是Go的「聯合創始人」Rob Pike某個會議上說的。
在Go中,推薦使用CSP實現並發,而不是習慣性的使用Lock,使用channel傳遞數據,達到多goroutine間共享數據的目的,也就是share memory by communicating。
所以,我們版本2,就使用channel的方式,來實現Peer的管理。
幻燈片25在版本1中,peers是大家都想訪問的,並且Host有4個方法,畫到了上面的圖中,我們看下怎麼用CSP實現。
peers需要在單獨的goroutine中,其他的4個方法在其他的goroutine中調用,它們之間進行通信。
我對使用CSP有一個好的實踐,就是把數據流動畫出來,並把要流動的數據標上,然後那些數據流動的線條,就是channel,線條上的數據就是channel要傳遞的數據,圖中也把這些線條和數據標上了。具體的細節,可以識別圖片中的二維碼,看看這篇老文,還有就是並不是所有的並發場景都適合使用channel,有些用鎖更好,這篇文章也有介紹。
幻燈片26重新定義Host,增加了4個channel,從上到下分別用於增加Peer、廣播消息、刪除Peer和停止Host。
幻燈片27Host增加了2個方法:
Start()用於啟動1個goroutine運行loop(),loop保存所有的peers。
Stop()用於關閉Host,讓loop退出。
幻燈片28左邊是loop()的實現,它從4個channel裡接收數據,然後做不同的操作。
右邊是AddPeer, RemovePeer, BroadcastMsg的實現。
利用1分鐘的事件,左右兩邊對照著看,理解增加1個Peer的全過程。
這就是版本2的全部實現了,思考一下版本2有什麼問題,原因是啥?
幻燈片29幻燈片30問題就是我們沒有實現GetPeer這個方法,聰明的你一定在Host的定義就發現了,只有增加、刪除和廣播消息的channel。
沒能實現GetPeer的原因下圖中進行了介紹,你有沒有解決辦法?
幻燈片31幻燈片32幻燈片33可能會有很多goroutine調用GetPeer,我們需要向每一個goroutine發送結果,這就需要每一個goroutine都需要對應的1個接收結果的channel。
所以我們可以增加1個query channel,channel裡傳遞Peer.ID和接收結果的channel。
還有沒有其他辦法?我們今天的主題First class function還有入場,你有辦法用這個特性實現嗎?
版本3幻燈片34First class function: 函數可以向變量一樣使用。那channel裡面是不是可以傳遞函數呢?當然可以。
幻燈片35我們可以建立一個channel,用這個channel向loop傳遞操作peers的函數,所以函數的入參是peers map[string]*Peer,無需返回值,因為函數是在loop裡面調用的,調用AddPeer等函數的goroutine是接收不到返回值的。我們把這個類型的函數定義為Operation。
Host修改為只有2個channel,stop功能如版本2,opCh用來傳遞Operation類型的函數。
幻燈片36loop函數可以簡化為左邊的形式了,右邊是AddPeer和RemovePeer,以AddPeer為例進行介紹,創建了一個匿名函數,向peers裡增加p,然後把函數發送到opCh。
幻燈片37BroadcastMsg與AddPeer類似。
幻燈片38我們重點看一下GetPeer,創建了retCh用於接收查詢的結果,創建了匿名函數進行查詢,並把查詢結果發送到retCh,然後啟動1個goroutine把匿名函數寫入到opCh,最後等待從retCh讀取查詢結果。
這樣就實現了向每個調用GetPeer的goroutine發送查詢結果。
總結幻燈片40總結都在上面了,不多說了。
友情提醒:這3種方式本身並無優劣之分,具體要用那種實現,要依賴自身的實際場景進行取捨。
源碼識別下圖二維碼。
幻燈片41PPT下載下載連結:http://img.lessisbetter.site/Go%E8%AF%AD%E8%A8%80%E6%80%9D%E7%BB%B4First-class-function.pdf
或閱讀原文下載。
雲象介紹廣告時間,雲象區塊鏈持續招人,歡迎來撩。
幻燈片42活動總結最後奉上Seekload關於本次活動的總結:Gopher杭州線下面基第一期。