這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
導語:Go語言的三個核心設計: interface 、goroutine 、 channel
less is more —— Wikipedia
interface
Go是一門面向介面編程的語言,interface的設計自然是重中之重。Go中對於interface設計的巧妙之處就在於空的interface可以被當作“Duck”類型使用,它使得Go這樣的靜態語言擁有了一定的動態性,卻又不損失靜態語言在型別安全方面擁有的編譯時間檢查的優勢。
source code
從底層實現來看,interface實際上是一個結構體,包含兩個成員。其中一個成員指標指向了包含類型資訊的地區,可以理解為虛表指標,而另一個則指向具體資料,也就是該interface實際引用的資料。
其中 interfacetype 包含了一些關於interface本身的資訊,_type表示具體實作類別型,在下文eface中會有詳細描述,bad 是一個狀態變數,fun是一個長度為1的指標數組,在 fun[0] 的地址後面依次儲存method對應的函數指標。go runtime 包裡面有一個hash表,通過這個hash表可以取得 itab,link跟inhash則是為了儲存hash表中對應的位置並設定標識。主要代碼如下:
Itab的結構如下:
其中 interfacetype 包含了一些關於interface本身的資訊,_type表示具體實作類別型,在下文eface中會有詳細描述,bad 是一個狀態變數,fun是一個長度為1的指標數組,在 fun[0] 的地址後面依次儲存method對應的函數指標。go runtime 包裡面有一個hash表,通過這個hash表可以取得 itab,link跟inhash則是為了儲存hash表中對應的位置並設定標識。主要代碼如下:
空介面的實現略有不同。Go中任何對象都可以表示為interface{},類似於C中的 void*,而且interface{}中存有類型資訊。
Type的結構如下:
i_example
關於interface的應用,下面舉個簡單的例子,是關於Go與Mysql資料庫互動的。
首先在mysql test庫中建立一張任務資訊表:
資料庫互動最基本的四個操作:增刪改查, 這裡以查詢為例:
Go來實現查詢這張表裡面的所有資料
其中:
這段代碼可以實現查表這個簡單的邏輯,但是有一個小小的問題就是,我們這張表結構比較簡單只有4個欄位,如果換一張有20+個欄位甚至更多的表來查詢的話,這段代碼就顯得太過於低效,這個時候我們便可以引入interface{}來進行最佳化。
最佳化後的代碼如下:
由於interface{}可以儲存任何類型的資料,所以通過構造args、values兩個數組,其中args的每個值指向values相應值的地址,來對資料進行批量的讀取及後續操作,值得注意的是Go是一門強型別的語言,而且不同的interface{}是存有不同的類型資訊的,在進行賦值等相關操作時需要進行類型轉換。
Go對於Mysql交易處理也提供了比較好的支援。一般的操作使用的是db對象的方法,事務則是使用sql.Tx對象。使用db的Begin方法可以建立tx對象。tx對象也有資料庫互動的Query,Exec和Prepare方法,與db的操作類似。查詢或修改的操作完畢之後,需要調用tx對象的Commit()提交或者Rollback()復原。
例如,現在需要利用事務對之前建立的user表進行update操作,代碼如下
注意: “ := “ 跟 “ = “兩個操作符不要弄混淆
如果不需要進行交易處理的話,update對應的代碼如下:
可以與上面增加事務操作的代碼進行對比,因為操作比較簡單所以也就增加了幾行代碼,以及將db對象換成了tx對象。
goroutine
並發:同一時間內處理(dealing with)不同的事情
並行:同一時間內做(doing)不同的事情
Go從語言層面就支援了並行,而goroutine則是Go並行設計的核心。本質上,goroutine就是協程,擁有獨立的可以自行管理的調用棧,可以把goroutine理解為輕量級的thread。但是thread是作業系統調度的,搶佔式的。goroutine是通過自己的調度器來調度的。
scheduler
Go的調度器實現了G-P-M調度模型,其中有三個重要的結構:M,P,G
M : Machine (OS thread)
P : Context (Go Scheduler)
G : Goroutine
底層的資料結構長這樣:
M、P 和 G 之間的互動可以通過下面這幾張來自go runtime scheduler的圖來展現
中看,有2個物理線程M,每一個M都擁有一個上下文P,也都有一個正在啟動並執行goroutine G。圖中灰色的那些G並沒有運行,而是出於ready的就緒態,正在等待被調度。由P來維護著這個runqueue隊列。
圖中的M1可能是被建立出來的,也可能是從線程緩衝中取出來的。當M0返回時,它必須嘗試擷取P來運行G,通常情況下,它會嘗試從其他的thread那裡”steal”一個P過來,失敗的話,它就把G放在一個global runqueue裡,然後自己會被放入線程緩衝裡。所有的P會周期性的檢查global runqueue,否則global runqueue上的G永遠無法執行。
另一種情況是P所分配的任務G很快就執行完了(因為分配不均),這就導致了某些P處於空閑狀態而系統卻依然在運行態。但如果global runqueue沒有任務G了,那麼P就不得不從其他的P那裡拿一些G來執行。通常情況下,如果P從其他的P那裡要偷一個任務的話,一般就‘steal’ runqueue的一半,這就確保了每個thread都能充分的使用。
P如何從其他P維護的隊列中”steal”到G呢?這就涉及到work-stealing演算法,關於該演算法的更多資訊可以參考這篇文章。
g_example
舉個簡單的例子來示範下goroutine是如何啟動並執行
這段代碼非常簡單,兩個不同的goroutine非同步運行
運行結果如下:
然後做個小小的改動,只是將main()中的兩個函數的位置互換,其餘代碼變:
會出現一件有意思的事情:
原因也很簡單,因為main()返回時, 並不會等待其他goroutine(非主goroutine)結束。對上面的例子, 主函數執行完第一個say()後,建立了一個新的goroutine沒來得及執行程式就結束了,所以會出現上面的運行結果。
channel
goroutine在相同的地址空間中運行,因此必須同步對共用記憶體的訪問。Go語言提供了一個很好的通訊機制channel,來滿足goroutine之間資料的通訊。channel與Unix shell 中的雙向管道有些類似:可以通過它發送或者接收值。
source code
其中waitq的結構如下
可以看到channel其實就是一個隊列加一個鎖。其中sendx和recvx可以看做生產者跟消費者隊列,分別儲存的是等待在channel上進行讀操作的goroutine和等待在channel上進行寫操作的goroutine,如所示。
寫channel (ch <- x)的具體實現如下(只選取了核心代碼):
具體可以分為三種情況:
- 有goroutine阻塞在channel上,而且chanbuf為空白,直接將資料發送給該goroutine上。
- chanbuf有空間可用:將資料放到chanbuf裡面。
- chanbuf沒有空間可用:阻塞當前goroutine。
讀channel( <-ch)和發送的操作類似,就不帖代碼展示了。
c_example
關於goroutine跟channel進行通訊的一個簡單的例子,邏輯很簡單:
這裡我們定義了兩個帶緩衝的channel jobs 和 results,如果把這兩個channel都換成不帶緩衝的,就會報錯,不過可以這樣進行處理就可以了:
比較常見的channel操作還有select , 存在多個channel的時候,可以通過select可以監聽channel上的資料流動。
因為 ch1 和 ch2 都為空白,所以 case1 和 case2 都不會讀取成功。 則 select 執行 default 語句。