騰訊大神分享 Go 語言之三駕馬車

導語:Go語言的三個核心設計: interface 、goroutine 、 channelmysql

less is more —— Wikipedia算法

interface

Go是一門面向接口編程的語言,interface的設計天然是重中之重。Go中對於interface設計的巧妙之處就在於空的interface能夠被看成「Duck」類型使用,它使得Go這樣的靜態語言擁有了必定的動態性,卻又不損失靜態語言在類型安全方面擁有的編譯時檢查的優點。sql

source code

從底層實現來看,interface其實是一個結構體,包含兩個成員。其中一個成員指針指向了包含類型信息的區域,能夠理解爲虛表指針,而另外一個則指向具體數據,也就是該interface實際引用的數據。shell

其中 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 語句。

相關文章
相關標籤/搜索