go語(yǔ)言筆記第二集,go 筆記

Go語(yǔ)言——sync.Map詳解

sync.Map是1.9才推薦的并發(fā)安全的map,除了互斥量以外,還運(yùn)用了原子操作,所以在這之前,有必要了解下 Go語(yǔ)言——原子操作

創(chuàng)新互聯(lián)堅(jiān)信:善待客戶,將會(huì)成為終身客戶。我們能堅(jiān)持多年,是因?yàn)槲覀円恢笨芍档眯刨?。我們從不忽悠初訪客戶,我們用心做好本職工作,不忘初心,方得始終。十余年網(wǎng)站建設(shè)經(jīng)驗(yàn)創(chuàng)新互聯(lián)是成都老牌網(wǎng)站營(yíng)銷服務(wù)商,為您提供成都網(wǎng)站制作、成都做網(wǎng)站、外貿(mào)營(yíng)銷網(wǎng)站建設(shè)、網(wǎng)站設(shè)計(jì)、H5建站、網(wǎng)站制作、成都品牌網(wǎng)站建設(shè)、微信平臺(tái)小程序開發(fā)服務(wù),給眾多知名企業(yè)提供過(guò)好品質(zhì)的建站服務(wù)。

go1.10\src\sync\map.go

entry分為三種情況:

從read中讀取key,如果key存在就tryStore。

注意這里開始需要加鎖,因?yàn)樾枰僮鱠irty。

條目在read中,首先取消標(biāo)記,然后將條目保存到dirty里。(因?yàn)闃?biāo)記的數(shù)據(jù)不在dirty里)

最后原子保存value到條目里面,這里注意read和dirty都有條目。

總結(jié)一下Store:

這里可以看到dirty保存了數(shù)據(jù)的修改,除非可以直接原子更新read,繼續(xù)保持read clean。

有了之前的經(jīng)驗(yàn),可以猜測(cè)下load流程:

與猜測(cè)的 區(qū)別 :

由于數(shù)據(jù)保存兩份,所以刪除考慮:

先看第二種情況。加鎖直接刪除dirty數(shù)據(jù)。思考下貌似沒什么問(wèn)題,本身就是臟數(shù)據(jù)。

第一種和第三種情況唯一的區(qū)別就是條目是否被標(biāo)記。標(biāo)記代表刪除,所以直接返回。否則CAS操作置為nil。這里總感覺少點(diǎn)什么,因?yàn)闂l目其實(shí)還是存在的,雖然指針nil。

看了一圈貌似沒找到標(biāo)記的邏輯,因?yàn)閯h除只是將他變成nil。

之前以為這個(gè)邏輯就是簡(jiǎn)單的將為標(biāo)記的條目拷貝給dirty,現(xiàn)在看來(lái)大有文章。

p == nil,說(shuō)明條目已經(jīng)被delete了,CAS將他置為標(biāo)記刪除。然后這個(gè)條目就不會(huì)保存在dirty里面。

這里其實(shí)就跟miss邏輯串起來(lái)了,因?yàn)閙iss達(dá)到閾值之后,dirty會(huì)全量變成read,也就是說(shuō)標(biāo)記刪除在這一步最終刪除。這個(gè)還是很巧妙的。

真正的刪除邏輯:

很繞。。。。

go筆記 - 字符串、數(shù)字、byte相互轉(zhuǎn)換

1、string到int

2、string到int64

3、string到int32

4、字符串 轉(zhuǎn) uint64

1、int到string

2、int64到string

3、uint64 轉(zhuǎn) 字符串

uint8,int8,uint16,int16,uint32,int32,float32,uint64,int64,float64

1、int64與uint64

2、int64轉(zhuǎn)float64

1、string 轉(zhuǎn)為[]byte

2、byte轉(zhuǎn)為string

從PHP 到Golang 的筆記 ( 轉(zhuǎn) )

———文章來(lái)源 YamiOdymel/PHP-to-Golang

PHP和模塊之間的關(guān)系令人感到煩躁,假設(shè)你要讀取 yaml 檔案,你需要有一個(gè) yaml 的模塊,為此,你還需要將其編譯然后將編譯后的模塊擺放至指定位置,之后換了一臺(tái)伺服器你還要重新編譯,這點(diǎn)到現(xiàn)在還是沒有改善;順帶一提之后出了PHP 7效能確實(shí)提升了許多(比Python 3快了些),但PHP仍令我感到臃腫,我覺得是時(shí)候

(轉(zhuǎn)行)了。

PHP 和Golang 的效能我想毋庸置疑是后者比較快(而且是以倍數(shù)來(lái)算),也許有的人會(huì)認(rèn)為兩種不應(yīng)該被放在一起比較,但Golang 本身就是偏向Web 開發(fā)的,所以這也是為什么我考慮轉(zhuǎn)用Golang 的原因,起初我的考慮有幾個(gè):Node.js 和Rust 還有最終被選定的Golang;先談?wù)凬ode.js 吧。

Node.js的效能可以說(shuō)是快上PHP 3.5倍至6倍左右 ,而且撰寫的語(yǔ)言還是JavaScript,蒸蚌,如此一來(lái)就不需要學(xué)習(xí)新語(yǔ)言了!搭配Babel更可以說(shuō)是萬(wàn)能,不過(guò)那跟「跳跳虎」一樣的Async邏輯還有那恐怖的Callback Hell,有人認(rèn)為前者是種優(yōu)點(diǎn),這點(diǎn)我不否認(rèn),但是對(duì)學(xué)習(xí)PHP的我來(lái)說(shuō)太過(guò)于"Mind Fuck",至于后者的Callback Hell雖然有Promise,但是那又是另一個(gè)「Then Hell」的故事了。相較于Golang之下,Node.js似乎就沒有那么吸引我了。你確實(shí)可以用Node.js寫出很多東西,不過(guò)那V8引擎的效能仍然有限,而且要學(xué)習(xí)新的事物,不就應(yīng)該是「全新」的嗎;)?

題外話: 為什么Node.js不適合大型和商業(yè)專案?

在拋棄改用Node.js 之后我曾經(jīng)花了一天的時(shí)間嘗試Rust 和Iron 框架,嗯??Rust 太強(qiáng)大了,強(qiáng)大到讓我覺得Rust 不應(yīng)該用在這里,這想法也許很蠢,但Rust 讓我覺得適合更應(yīng)該拿來(lái)用在系統(tǒng)或者是部分底層的地方,而不應(yīng)該是網(wǎng)路服務(wù)。

Golang是我最終的選擇,主要在于我花了一天的時(shí)間來(lái)研究的時(shí)候意外地發(fā)現(xiàn)Golang夭壽簡(jiǎn)潔( 關(guān)鍵字只有25個(gè) ),相較之下Rust太過(guò)于「強(qiáng)大」令我怯步;而且Golang帶有許多工具,例如 go fmt 會(huì)自動(dòng)幫你整理程式碼、 go doc 會(huì)自動(dòng)幫你生產(chǎn)文件、 go test 可以自動(dòng)單元測(cè)試并生產(chǎn)覆蓋率報(bào)表、也有 go get 套件管理工具(雖然沒有版本功能),不過(guò)都很實(shí)用,而且也不需要加上分號(hào)( ; ),真要說(shuō)不好的地方??大概就是強(qiáng)迫你花括號(hào)不能換行放吧(沒錯(cuò),我就是花括號(hào)會(huì)換行放的人)。

當(dāng)我在撰寫這份文件的時(shí)候 我會(huì)先假設(shè)你有一定的基礎(chǔ) ,你可以先閱讀下列的手冊(cè),他們都很不錯(cuò)。

你能夠在PHP 里面想建立一個(gè)變數(shù)的時(shí)候就直接建立,夭壽贊,是嗎?

蒸蚌!那么Golang 呢?在Golang 中變數(shù)分為幾類:「新定義」、「預(yù)先定義」、「自動(dòng)新定義」、「覆蓋」。讓我們來(lái)看看范例:

在PHP中你會(huì)很常用到 echo 來(lái)顯示文字,像這樣。

然而在Golang中你會(huì)需要 fmt 套件,關(guān)于「什么是套件」的說(shuō)明你可以在文章下述了解。

這很簡(jiǎn)單,而且兩個(gè)語(yǔ)言的用法相差甚少,下面這是PHP:

只是Golang 稍微聒噪了一點(diǎn),你必須在函式后面宣告他最后會(huì)回傳什么資料型別。

在PHP 中你要回傳多個(gè)資料你就會(huì)用上陣列,然后將資料放入陣列里面,像這樣。

然而在Golang 中你可以不必用到一個(gè)陣列,函式可以一次回傳多個(gè)值:

兩個(gè)語(yǔ)言的撰寫方式不盡相同。

主要是PHP 的陣列能做太多事情了,所以在PHP 里面要儲(chǔ)存什么用陣列就好了。

在Golang里??沒有這么萬(wàn)能的東西,首先要先了解Golang中有這些型態(tài): array , slice , map , interface ,

你他媽的我到底看了三洨,首先你要知道Golang是個(gè)強(qiáng)型別語(yǔ)言,意思是你的陣列中 只能有一種型態(tài) ,什么意思?當(dāng)你決定這個(gè)陣列是用來(lái)擺放字串資料的時(shí)候,你就只能在里面放字串。沒有數(shù)值、沒有布林值,就像你沒有女朋友一樣。

先撇開PHP 的「萬(wàn)能陣列」不管,Golang 中的陣列既單純卻又十分腦殘,在定義一個(gè)陣列的時(shí)候,你必須給他一個(gè)長(zhǎng)度還有其內(nèi)容存放的資料型態(tài),你的陣列內(nèi)容不一定要填滿其長(zhǎng)度,但是你的陣列內(nèi)容不能超過(guò)你當(dāng)初定義的長(zhǎng)度。

切片??這聽起來(lái)也許很奇怪,但是你確實(shí)可以「切」他,讓我們先談?wù)劇盖衅贡绕稹戈嚵小挂迷谀睦铮骸改悴挥枚x其最大長(zhǎng)度,而且你可以直接賦予值」,沒了。

我們剛才有提到你可以「切」他,記得嗎?這有點(diǎn)像是PHP中的 array_slice() ,但是Golang直接讓Slice「內(nèi)建」了這個(gè)用法,其用法是: slice[開始:結(jié)束] 。

在PHP中倒是沒有那么方便,在下列PHP范例中你需要不斷地使用 array_slice() 。

你可以把「映照」看成是一個(gè)有鍵名和鍵值的陣列,但是記?。骸改阈枰孪榷x其鍵名、鍵值的資料型態(tài)」,這仍限制你沒辦法在映照中存放多種不同型態(tài)的資料。

在Golang里可就沒這么簡(jiǎn)單了,你需要先用 make() 宣告 map 。

也許你不喜歡「接口」這個(gè)詞,但用「介面」我怕會(huì)誤導(dǎo)大眾,所以,是的,接下來(lái)我會(huì)繼續(xù)稱其為「接口」。還記得你可以在PHP 的關(guān)聯(lián)陣列里面存放任何型態(tài)的資料嗎,像下面這樣?

現(xiàn)在你有福了!正因?yàn)镚olang中的 interface{} 可以接受任何內(nèi)容,所以你可以把它拿來(lái)存放任何型態(tài)的資料。

有時(shí)候你也許會(huì)有個(gè)不定值的變數(shù),在PHP 里你可以直接將一個(gè)變數(shù)定義成字串、數(shù)值、空值、就像你那變心的女友一樣隨時(shí)都在變。

在Golang中你必須給予變數(shù)一個(gè)指定的資料型別,不過(guò)還記得剛才提到的:「Golang中有個(gè) interface{} 能夠 存放任何事物 」嗎( 雖然也不是真的任何事物啦?? )?

當(dāng)我們程式中不需要繼續(xù)使用到某個(gè)資源或是發(fā)生錯(cuò)誤的時(shí)候,我們索性會(huì)將其關(guān)閉或是拋棄來(lái)節(jié)省資源開銷,例如PHP 里的讀取檔案:

在Golang中,你可以使用 defer 來(lái)在函式結(jié)束的時(shí)候自動(dòng)執(zhí)行某些程式(其執(zhí)行方向?yàn)榉聪?。所以你就不需要在函式最后面結(jié)束最前面的資源。

defer 可以被稱為「推遲執(zhí)行」,實(shí)際上就是在函式結(jié)束后會(huì)「反序」執(zhí)行的東西,例如你按照了這樣的順序定義 defer : A-B-C-D ,那么執(zhí)行的順序其實(shí)會(huì)是 D-C-B-A ,這用在程式結(jié)束時(shí)還蠻有用的,讓我們看看Golang如何改善上述范例。

這東西很邪惡,不是嗎?又不是在寫B(tài)ASIC,不過(guò)也許有時(shí)候你會(huì)在PHP 用上呢。但是拜托,不要。

Golang中僅有 for 一種回圈但卻能夠達(dá)成 foreach 、 while 、 for 多種用法。普通 for 回圈寫法在兩個(gè)語(yǔ)言中都十分相近。

在Golang請(qǐng)記得:如果你的 i 先前并不存在,那么你就需要定義它,所以下面這個(gè)范例你會(huì)看見 i := 0 。

在PHP里, foreach() 能夠直接給你值和鍵名,用起來(lái)十分簡(jiǎn)單。

Golang里面雖然僅有 for() 但卻可以使用 range 達(dá)成和PHP一樣的 foreach 方式。

一個(gè) while(條件) 回圈在PHP里面可以不斷地執(zhí)行區(qū)塊中的程式,直到 條件 為 false 為止。

在Golang里也有相同的做法,但仍是透過(guò) for 回圈,請(qǐng)注意這個(gè) for 回圈并沒有任何的分號(hào)( ; ),而且一個(gè)沒有條件的 for 回圈會(huì)一直被執(zhí)行。

PHP中有 do .. while() 回圈可以先做區(qū)塊中的動(dòng)作。

在Golang中則沒有相關(guān)函式,但是你可以透過(guò)一個(gè)無(wú)止盡的 for 回圈加上條件式來(lái)讓他結(jié)束回圈。

要是你真的希望完全符合像是PHP那樣的設(shè)計(jì)方式,或者你可以在Golang中使用很邪惡的 goto 。

在PHP中我們可以透過(guò) date() 像這樣取得目前的日期。

在Golang就稍微有趣點(diǎn)了,因?yàn)镚olang中并不是以 Y-m-d 這種格式做為定義,而是 1 、 2 、 3 ,這令你需要去翻閱文件,才能夠知道 1 的定義是代表什么。

俗話說(shuō):「爆炸就是藝術(shù)」,可愛的PHP用詞真的很大膽,像是: explode() (爆炸)、 die() (死掉),回歸正傳,如果你想在PHP里面將字串切割成陣列,你可以這么做。

簡(jiǎn)單的就讓一個(gè)字串給「爆炸」了,那么Golang 呢?

對(duì)了,記得引用 strings 套件。

這真的是很常用到的功能,就像物件一樣有著鍵名和鍵值,在PHP 里面你很簡(jiǎn)單的就能靠陣列(Array)辦到。

真是太棒了,那么Golang呢?用 map 是差不多啦。如果有必要的話,你可以稍微復(fù)習(xí)一下先前提到的「多資料儲(chǔ)存型態(tài)-Stores」。

你很常會(huì)在PHP里面用 isset() 檢查一個(gè)索引是否存在,不是嗎?

在Golang里面很簡(jiǎn)單的能夠這樣辦到(僅適用于 map )。

指針(有時(shí)也做參照)是一個(gè)像是「變數(shù)別名」的方法,這種方法讓你不用整天覆蓋舊的變數(shù),讓我們假設(shè) A = 1; B = A; 這個(gè)時(shí)候 B 會(huì)復(fù)制一份 A 且兩者不相干,倘若你希望修改 B 的時(shí)候?qū)嶋H上也會(huì)修改到 A 的值,就會(huì)需要指針。

指針比起復(fù)制一個(gè)變數(shù),他會(huì)建立一個(gè)指向到某個(gè)變數(shù)的記憶體位置,這也就是為什么你改變指針,實(shí)際上是在改變某個(gè)變數(shù)。

在Golang你需要用上 * 還有 符號(hào)。

有些時(shí)候你會(huì)回傳一個(gè)陣列,這個(gè)陣列里面可能有資料還有錯(cuò)誤代號(hào),而你會(huì)用條件式判斷錯(cuò)誤代號(hào)是否非空值。

在Golang中函式可以一次回傳多個(gè)值。為此,你不需要真的回傳一個(gè)陣列,不過(guò)要注意的是你將會(huì)回傳一個(gè)屬于 error 資料型態(tài)的錯(cuò)誤,所以你需要引用 errors 套件來(lái)幫助你做這件事。

該注意的是Golang沒有 try .. catch ,因?yàn)?Golang推薦這種錯(cuò)誤處理方式 ,你應(yīng)該在每一次執(zhí)行可能會(huì)發(fā)生錯(cuò)誤的程式時(shí)就處理錯(cuò)誤,而非后來(lái)用 try 到處包覆你的程式。

在 if 條件式里宣告變數(shù)會(huì)讓你只能在 if 內(nèi)部使用這個(gè)變數(shù),而不會(huì)污染到全域范圍。

也許你在PHP中更常用的會(huì)是 try .. catch ,在大型商業(yè)邏輯時(shí)經(jīng)??匆娙绱说赜梅ǎ瑢?shí)際上這種用法令人感到聒噪(因?yàn)槟銜?huì)需要一堆 try 區(qū)塊):

Golang中并沒有 try .. catch ,實(shí)際上Golang也 不鼓勵(lì)這種行為 (Golang推薦逐一處理錯(cuò)誤的方式),倘若你真想辦倒像是捕捉異常這樣的方式,你確實(shí)可以使用Golang中另類處理錯(cuò)誤的方式(可以的話盡量避免使用這種方式): panic() , recover() , defer 。

你可以把 panic() 當(dāng)作是 throw (丟出錯(cuò)誤),而這跟PHP的 exit() 有87%像,一但你執(zhí)行了 panic() 你的程式就會(huì)宣告而終,但是別擔(dān)心,因?yàn)槌淌浇Y(jié)束的時(shí)候會(huì)呼叫 defer ,所以我們接下來(lái)要在 defer 停止 panic() 。

關(guān)于 defer 上述已經(jīng)有提到了,他是一個(gè)反向執(zhí)行的宣告,會(huì)在函式結(jié)束后被執(zhí)行,當(dāng)你呼叫了 panic() 結(jié)束程式的時(shí)候,也就會(huì)開始執(zhí)行 defer ,所以我們要在 defer 內(nèi)使用 recover() 讓程式不再繼續(xù)進(jìn)行結(jié)束動(dòng)作,這就像是捕捉異常。

recover() 可以看作 catch (捕捉),我們要在 defer 里面用 recover() 解決 panic() ,如此一來(lái)程式就會(huì)回歸正常而不會(huì)被結(jié)束。

還記得在PHP里要引用一堆檔案的日子嗎?到處可見的 require() 或是 include() ?到了Golang這些都不見了,取而代之的是「套件(Package)」?,F(xiàn)在讓我們來(lái)用PHP解釋一下。

這看起來(lái)很正常對(duì)吧?但假設(shè)你有一堆檔案,這馬上就成了 Include Hell ,讓我們看看Golang怎么透過(guò)「套件」解決這個(gè)問(wèn)題。

「 蛤???殺?。??? 」你可能如此地說(shuō)道。是的, main.go 中除了引用 fmt 套件( 為了要輸出結(jié)果用的套件 )之外完全沒有引用到 a.go 。

「 蛤???殺???????? 」你仿佛回到了幾秒鐘前的自己。

既然沒有引用其他檔案,為什么 main.go 可以輸出 foo 呢?注意到了嗎, 兩者都是屬于 main 套件 ,因此 他們共享同一個(gè)區(qū)域 ,所以接下來(lái)要介紹的是什么叫做「套件」。

套件是每一個(gè) .go 檔案都必須聲明在Golang原始碼中最開端的東西,像下面這樣:

這意味著目前的檔案是屬于 main 套件( 你也可以依照你的喜好命名 ),那么要如何讓同個(gè)套件之間的函式溝通呢?

接著是Golang;注意!你不需要引用任何檔案,因?yàn)橄铝袃蓚€(gè)檔案同屬一個(gè)套件。

一個(gè)由「套件」所掌握的世界,比起PHP的 include() 和 require() 還要好太多了,對(duì)嗎?

在Golang 中沒有引用單獨(dú)檔案的方式,你必須匯入一整個(gè)套件,而且你要記?。骸敢欢銋R入了,你就一定要使用它」,像下面這樣。

假如你不希望使用你匯入的套件,你只是為了要觸發(fā)那個(gè)套件的 main() 函式而引用的話??,那么你可以在前面加上一個(gè)底線( _ )。

如果你的套件出現(xiàn)了名稱沖突,你可以在套件來(lái)源前面給他一個(gè)新的名稱。

現(xiàn)在你知道可以匯入套件了,那么什么是「匯出」?同個(gè)套件內(nèi)的函式還有共享變數(shù)確實(shí)可以直接用,但那 并不表示可以給其他套件使用 ,其方法取決于 函式/變數(shù)的「開頭大小寫」 。

是的。 Golang依照一個(gè)函式/變數(shù)的開頭大小寫決定這個(gè)東西是否可供「匯出」 。

這用在區(qū)別函式的時(shí)候格外有用,因?yàn)樾戦_頭的任何事物都是不供匯出的,反之,大寫開頭的任何事物都是用來(lái)匯出供其他套件使用的。

一開始可能會(huì)覺得這是什么奇異的規(guī)定,但寫久之后,你就能發(fā)現(xiàn)比起JavaScript和Python以「底線為開頭的命名方式」還要來(lái)得更好;比起成天宣告 public 、 private 、 protected 還要來(lái)得更快。

在Golang 中沒有類別,但有所謂的「建構(gòu)體(Struct)」和「接口(Interface)」,這就能夠滿足幾乎所有的需求了,這也是為什么我認(rèn)為Golang 很簡(jiǎn)潔卻又很強(qiáng)大的原因。

讓我們先用PHP 建立一個(gè)類別,然后看看Golang 怎么解決這個(gè)問(wèn)題。

雖然Golang沒有類別,但是「建構(gòu)體(Struct)」就十分地堪用了,首先你要知道在Golang中「類別」的成員還有方法都是在「類別」外面所定義的,這跟PHP在類別內(nèi)定義的方式有所不同,在Golang中還有一點(diǎn),那就是他們沒有 public 、 private 、 protected 的種類。

在PHP中,當(dāng)有一個(gè)類別被 new 的時(shí)候會(huì)自動(dòng)執(zhí)行該類別內(nèi)的建構(gòu)子( __construct() ),通常你會(huì)用這個(gè)來(lái)初始化一些類別內(nèi)部的值。

但是在Golang 里因?yàn)闆]有類別,也就沒有建構(gòu)子,不巧的是建構(gòu)體本身也不帶有建構(gòu)子的特性,這個(gè)時(shí)候你只能自己在外部建立一個(gè)建構(gòu)用函式。

讓我們假設(shè)你有兩個(gè)類別,你會(huì)把其中一個(gè)類別傳入到另一個(gè)類別里面使用,廢話不多說(shuō)!先上個(gè)PHP 范例(為了簡(jiǎn)短篇幅我省去了換行)。

在Golang中你也有相同的用法,但是請(qǐng)記得:「 任何東西都是在「類別」外完成建構(gòu)的 」。

在PHP 中沒有相關(guān)的范例,這部分會(huì)以剛才「嵌入」章節(jié)中的Golang 范例作為解說(shuō)對(duì)象。

你可以看見Golang在進(jìn)行 Foo 嵌入 Bar 的時(shí)候,會(huì)自動(dòng)將 Foo 的成員暴露在 Bar 底下,那么假設(shè)「雙方之間有相同的成員名稱」呢?

這個(gè)時(shí)候被嵌入的成員就會(huì)被「遮蔽」,下面是個(gè)實(shí)際范例,還有你如何解決遮蔽問(wèn)題:

雖然都是呼叫同一個(gè)函式,但是這個(gè)函式可以針對(duì)不同的資料來(lái)源做出不同的舉動(dòng),這就是多形。你也能夠把這看作是:「訊息的意義由接收者定義,而不是傳送者」。

目前PHP 中沒有真正的「多形」,不過(guò)你仍可以做出同樣的東西。

嗯??那么Golang呢?實(shí)際上更簡(jiǎn)單而且更有條理了,在Golang中有 interface 可以幫忙完成這個(gè)工作。

如果你對(duì)Interface還不熟悉,可以試著查看「 解釋Golang中的Interface到底是什么 」文章。

謝謝你看到這里,可惜這篇文章卻沒有說(shuō)出Golang 最重要的賣點(diǎn):「Goroutine」和「Channel」

Golang 語(yǔ)言深入理解:channel

本文是對(duì) Gopher 2017 中一個(gè)非常好的 Talk?: [Understanding Channel](GopherCon 2017: Kavya Joshi - Understanding Channels) 的學(xué)習(xí)筆記,希望能夠通過(guò)對(duì) channel 的關(guān)鍵特性的理解,進(jìn)一步掌握其用法細(xì)節(jié)以及 Golang 語(yǔ)言設(shè)計(jì)哲學(xué)的管窺蠡測(cè)。

channel 是可以讓一個(gè) goroutine 發(fā)送特定值到另一個(gè) gouroutine 的通信機(jī)制。

原生的 channel 是沒有緩存的(unbuffered channel),可以用于 goroutine 之間實(shí)現(xiàn)同步。

關(guān)閉后不能再寫入,可以讀取直到 channel 中再?zèng)]有數(shù)據(jù),并返回元素類型的零值。

gopl/ch3/netcat3

首先從 channel 是怎么被創(chuàng)建的開始:

在 heap 上分配一個(gè) hchan 類型的對(duì)象,并將其初始化,然后返回一個(gè)指向這個(gè) hchan 對(duì)象的指針。

理解了 channel 的數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn),現(xiàn)在轉(zhuǎn)到 channel 的兩個(gè)最基本方法: sends 和 receivces ,看一下以上的特性是如何體現(xiàn)在 sends 和 receives 中的:

假設(shè)發(fā)送方先啟動(dòng),執(zhí)行 ch - task0 :

如此為 channel 帶來(lái)了 goroutine-safe 的特性。

在這樣的模型里, sender goroutine - channel - receiver goroutine 之間, hchan 是唯一的共享內(nèi)存,而這個(gè)唯一的共享內(nèi)存又通過(guò) mutex 來(lái)確保 goroutine-safe ,所有在隊(duì)列中的內(nèi)容都只是副本。

這便是著名的 golang 并發(fā)原則的體現(xiàn):

發(fā)送方 goroutine 會(huì)阻塞,暫停,并在收到 receive 后才恢復(fù)。

goroutine 是一種 用戶態(tài)線程 , 由 Go runtime 創(chuàng)建并管理,而不是操作系統(tǒng),比起操作系統(tǒng)線程來(lái)說(shuō),goroutine更加輕量。

Go runtime scheduler 負(fù)責(zé)將 goroutine 調(diào)度到操作系統(tǒng)線程上。

runtime scheduler 怎么將 goroutine 調(diào)度到操作系統(tǒng)線程上?

當(dāng)阻塞發(fā)生時(shí),一次 goroutine 上下文切換的全過(guò)程:

然而,被阻塞的 goroutine 怎么恢復(fù)過(guò)來(lái)?

阻塞發(fā)生時(shí),調(diào)用 runtime sheduler 執(zhí)行 gopark 之前,G1 會(huì)創(chuàng)建一個(gè) sudog ,并將它存放在 hchan 的 sendq 中。 sudog 中便記錄了即將被阻塞的 goroutine G1 ,以及它要發(fā)送的數(shù)據(jù)元素 task4 等等。

接收方 將通過(guò)這個(gè) sudog 來(lái)恢復(fù) G1

接收方 G2 接收數(shù)據(jù), 并發(fā)出一個(gè) receivce ,將 G1 置為 runnable :

同樣的, 接收方 G2 會(huì)被阻塞,G2 會(huì)創(chuàng)建 sudoq ,存放在 recvq ,基本過(guò)程和發(fā)送方阻塞一樣。

不同的是,發(fā)送方 G1如何恢復(fù)接收方 G2,這是一個(gè)非常神奇的實(shí)現(xiàn)。

理論上可以將 task 入隊(duì),然后恢復(fù) G2, 但恢復(fù) G2后,G2會(huì)做什么呢?

G2會(huì)將隊(duì)列中的 task 復(fù)制出來(lái),放到自己的 memory 中,基于這個(gè)思路,G1在這個(gè)時(shí)候,直接將 task 寫到 G2的 stack memory 中!

這是違反常規(guī)的操作,理論上 goroutine 之間的 stack 是相互獨(dú)立的,只有在運(yùn)行時(shí)可以執(zhí)行這樣的操作。

這么做純粹是出于性能優(yōu)化的考慮,原來(lái)的步驟是:

優(yōu)化后,相當(dāng)于減少了 G2 獲取鎖并且執(zhí)行 memcopy 的性能消耗。

channel 設(shè)計(jì)背后的思想可以理解為 simplicity 和 performance 之間權(quán)衡抉擇,具體如下:

queue with a lock prefered to lock-free implementation:

比起完全 lock-free 的實(shí)現(xiàn),使用鎖的隊(duì)列實(shí)現(xiàn)更簡(jiǎn)單,容易實(shí)現(xiàn)

Go語(yǔ)言使用 map 時(shí)盡量不要在 big map 中保存指針

不知道你有沒有聽過(guò)這么一句:在使用 map 時(shí)盡量不要在 big map 中保存指針。好吧,你現(xiàn)在已經(jīng)聽過(guò)了:)為什么呢?原因在于 Go 語(yǔ)言的垃圾回收器會(huì)掃描標(biāo)記 map 中的所有元素,GC 開銷相當(dāng)大,直接GG。

這兩天在《Mastering Go》中看到 GC 這一章節(jié)里面對(duì)比 map 和 slice 在垃圾回收中的效率對(duì)比,書中只給出結(jié)論沒有說(shuō)明理由,這我是不能忍的,于是有了這篇學(xué)習(xí)筆記。扯那么多,Show Your Code

這是一個(gè)簡(jiǎn)單的測(cè)試程序,保存字符串的 map 和 保存整形的 map GC 的效率相差幾十倍,是不是有同學(xué)會(huì)說(shuō)明明保存的是 string 哪有指針?這個(gè)要說(shuō)到 Go 語(yǔ)言中 string 的底層實(shí)現(xiàn)了,源碼在 src/runtime/string.go里,可以看到 string 其實(shí)包含一個(gè)指向數(shù)據(jù)的指針和一個(gè)長(zhǎng)度字段。注意這里的是否包含指針,包括底層的實(shí)現(xiàn)。

Go 語(yǔ)言的 GC 會(huì)遞歸遍歷并標(biāo)記所有可觸達(dá)的對(duì)象,標(biāo)記完成之后將所有沒有引用的對(duì)象進(jìn)行清理。掃描到指針就會(huì)往下接著尋找,一直到結(jié)束。

Go 語(yǔ)言中 map 是基于 數(shù)組和鏈表 的數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)的,通過(guò) 優(yōu)化的拉鏈法 解決哈希沖突,每個(gè) bucket 可以保存 8 對(duì)鍵值,在 8 個(gè)鍵值對(duì)數(shù)據(jù)后面有一個(gè) overflow 指針,因?yàn)橥爸凶疃嘀荒苎b 8 個(gè)鍵值對(duì),如果有多余的鍵值對(duì)落到了當(dāng)前桶,那么就需要再構(gòu)建一個(gè)桶(稱為溢出桶),通過(guò) overflow 指針鏈接起來(lái)。

因?yàn)?overflow 指針的緣故,所以無(wú)論 map 保存的是什么,GC 的時(shí)候就會(huì)把所有的 bmap 掃描一遍,帶來(lái)巨大的 GC 開銷。官方 issues 就有關(guān)于這個(gè)問(wèn)題的討論, runtime: Large maps cause significant GC pauses #9477

無(wú)腦機(jī)翻如下:

如果我們有一個(gè)map [k] v,其中k和v都不包含指針,并且我們想提高掃描性能,則可以執(zhí)行以下操作。

將“ allOverflow [] unsafe.Pointer”添加到 hmap 并將所有溢出存儲(chǔ)桶存儲(chǔ)在其中。 然后將 bmap 標(biāo)記為noScan。 這將使掃描非??欤?yàn)槲覀儾粫?huì)掃描任何用戶數(shù)據(jù)。

實(shí)際上,它將有些復(fù)雜,因?yàn)槲覀冃枰獜腶llOverflow中刪除舊的溢出桶。 而且它還會(huì)增加 hmap 的大小,因此也可能需要重新整理數(shù)據(jù)。

最終官方在 hmap 中增加了 overflow 相關(guān)字段完成了上面的優(yōu)化,這是具體的 commit 地址。

下面看下具體是如何實(shí)現(xiàn)的,源碼基于 go1.15,src/cmd/compile/internal/gc/reflect.go 中

通過(guò)注釋可以看出,如果 map 中保存的鍵值都不包含指針(通過(guò) Haspointers 判斷),就使用一個(gè) uintptr 類型代替 bucket 的指針用于溢出桶 overflow 字段,uintptr 類型在 GO 語(yǔ)言中就是個(gè)大小可以保存得下指針的整數(shù),不是指針,就相當(dāng)于實(shí)現(xiàn)了 將 bmap 標(biāo)記為 noScan, GC 的時(shí)候就不會(huì)遍歷完整個(gè) map 了。隨著不斷的學(xué)習(xí),愈發(fā)感慨 GO 語(yǔ)言中很多模塊設(shè)計(jì)得太精妙了。

差不多說(shuō)清楚了,能力有限,有不對(duì)的地方歡迎留言討論,源碼位置還是問(wèn)的群里大佬 _

名稱欄目:go語(yǔ)言筆記第二集,go 筆記
分享地址:http://muchs.cn/article34/hcpspe.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站設(shè)計(jì)、面包屑導(dǎo)航網(wǎng)站收錄、網(wǎng)站內(nèi)鏈網(wǎng)站建設(shè)、小程序開發(fā)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)

手機(jī)網(wǎng)站建設(shè)