天天看點

Go 語言之三駕馬車

導語:Go語言的三個核心設計: interface 、goroutine 、 channel less is more —— Wikipedia

Go是一門面向接口程式設計的語言,interface的設計自然是重中之重。Go中對于interface設計的巧妙之處就在于空的interface可以被當作“Duck”類型使用,它使得Go這樣的靜态語言擁有了一定的動态性,卻又不損失靜态語言在類型安全方面擁有的編譯時檢查的優勢。

source code

從底層實作來看,interface實際上是一個結構體,包含兩個成員。其中一個成員指針指向了包含類型資訊的區域,可以了解為虛表指針,而另一個則指向具體資料,也就是該interface實際引用的資料。

Go 語言之三駕馬車

其中 interfacetype 包含了一些關于interface本身的資訊,_type表示具體實作類型,在下文eface中會有較長的描述,bad 是一個狀态變量,fun是一個長度為1的指針數組,在 fun0 的位址後面依次儲存method對應的函數指針。go runtime 包裡面有一個hash表,通過這個hash表可以取得 itab,link跟inhash則是為了儲存hash表中對應的位置并設定辨別。主要代碼如下:

Itab的結構如下:

Go 語言之三駕馬車
Go 語言之三駕馬車

空接口的實作略有不同。Go中任何對象都可以表示為interface{},類似于C中的 void*,而且interface{}中存有類型資訊。

Go 語言之三駕馬車

Type的結構如下:

Go 語言之三駕馬車

關于interface的應用,下面舉個簡單的例子,是關于Go與Mysql資料庫互動的。

首先在mysql test庫中建立一張任務資訊表:

Go 語言之三駕馬車

資料庫互動最基本的四個操作:增删改查, 這裡以查詢為例:

Go來實作查詢這張表裡面的所有資料

Go 語言之三駕馬車

其中:

Go 語言之三駕馬車

這段代碼可以實作查表這個簡單的邏輯,但是有一個小小的問題就是,我們這張表結構比較簡單隻有4個字段,如果換一張有20+個字段甚至更多的表來查詢的話,這段代碼就顯得太過于低效,這個時候我們便可以引入interface{}來進行優化。

優化後的代碼如下:

Go 語言之三駕馬車

由于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操作,代碼如下

Go 語言之三駕馬車

注意: “ := “ 跟 “ = “兩個操作符不要弄混淆

如果不需要進行事務處理的話,update對應的代碼如下:

Go 語言之三駕馬車

可以與上面增加事務操作的代碼進行對比,因為操作比較簡單是以也就增加了幾行代碼,以及将db對象換成了tx對象。

并發:同一時間内處理(dealing with)不同的事情

并行:同一時間内做(doing)不同的事情

Go從語言層面就支援了并行,而goroutine則是Go并行設計的核心。本質上,goroutine就是協程,擁有獨立的可以自行管理的調用棧,可以把goroutine了解為輕量級的thread。但是thread是作業系統排程的,搶占式的。goroutine是通過自己的排程器來排程的。

Go的排程器實作了G-P-M排程模型,其中有三個重要的結構:M,P,G

M : Machine (OS thread)

P : Context (Go Scheduler)

G : Goroutine

底層的資料結構長這樣:

Go 語言之三駕馬車
Go 語言之三駕馬車
Go 語言之三駕馬車

M、P 和 G 之間的互動可以通過下面這幾張來自go runtime scheduler的圖來展現

Go 語言之三駕馬車

上圖中看,有2個實體線程M,每一個M都擁有一個上下文P,也都有一個正在運作的goroutine G。圖中灰色的那些G并沒有運作,而是出于ready的就緒态,正在等待被排程。由P來維護着這個runqueue隊列。

Go 語言之三駕馬車

圖中的M1可能是被建立出來的,也可能是從線程緩存中取出來的。當M0傳回時,它必須嘗試擷取P來運作G,通常情況下,它會嘗試從其他的thread那裡”steal”一個P過來,失敗的話,它就把G放在一個global runqueue裡,然後自己會被放入線程緩存裡。所有的P會周期性的檢查global runqueue,否則global runqueue上的G永遠無法執行。

Go 語言之三駕馬車

另一種情況是P所配置設定的任務G很快就執行完了(因為配置設定不均),這就導緻了某些P處于空閑狀态而系統卻依然在運作态。但如果global runqueue沒有任務G了,那麼P就不得不從其他的P那裡拿一些G來執行。通常情況下,如果P從其他的P那裡要偷一個任務的話,一般就‘steal’ runqueue的一半,這就確定了每個thread都能充分的使用。

P如何從其他P維護的隊列中”steal”到G呢?這就涉及到work-stealing算法,關于該算法的更多資訊可以參考這篇文章。

舉個簡單的例子來示範下goroutine是如何運作的

Go 語言之三駕馬車

這段代碼非常簡單,兩個不同的goroutine異步運作

運作結果如下:

Go 語言之三駕馬車

然後做個小小的改動,隻是将main()中的兩個函數的位置互換,其餘代碼變:

Go 語言之三駕馬車

會出現一件有意思的事情:

Go 語言之三駕馬車

原因也很簡單,因為main()傳回時, 并不會等待其他goroutine(非主goroutine)結束。對上面的例子, 主函數執行完第一個say()後,建立了一個新的goroutine沒來得及執行程式就結束了,是以會出現上面的運作結果。

goroutine在相同的位址空間中運作,是以必須同步對共享記憶體的通路。Go語言提供了一個很好的通信機制channel,來滿足goroutine之間資料的通信。channel與Unix shell 中的雙向管道有些類似:可以通過它發送或者接收值。

Go 語言之三駕馬車

其中waitq的結構如下

Go 語言之三駕馬車

可以看到channel其實就是一個隊列加一個鎖。其中sendx和recvx可以看做生産者跟消費者隊列,分别儲存的是等待在channel上進行讀操作的goroutine和等待在channel上進行寫操作的goroutine,如下圖所示。

Go 語言之三駕馬車

寫channel (ch <- x)的具體實作如下(隻選取了核心代碼):

具體可以分為三種情況:

有goroutine阻塞在channel上,而且chanbuf為空,直接将資料發送給該goroutine上。

chanbuf有空間可用:将資料放到chanbuf裡面。

chanbuf沒有空間可用:阻塞目前goroutine。

Go 語言之三駕馬車
Go 語言之三駕馬車
Go 語言之三駕馬車

讀channel( <-ch)和發送的操作類似,就不帖代碼展示了。

關于goroutine跟channel進行通信的一個簡單的例子,邏輯很簡單:

Go 語言之三駕馬車

這裡我們定義了兩個帶緩存的channel jobs 和 results,如果把這兩個channel都換成不帶緩存的,就會報錯,不過可以這樣進行處理就可以了:

Go 語言之三駕馬車

比較常見的channel操作還有select , 存在多個channel的時候,可以通過select可以監聽channel上的資料流動。

Go 語言之三駕馬車

因為 ch1 和 ch2 都為空,是以 case1 和 case2 都不會讀取成功。 則 select 執行 default 語句。