map 是Go語言中基礎(chǔ)的數(shù)據(jù)結(jié)構(gòu),在日常的使用中經(jīng)常被用到。但是它底層是如何實現(xiàn)的呢?
成都創(chuàng)新互聯(lián)是一家專注于網(wǎng)站建設(shè)、網(wǎng)站制作與策劃設(shè)計,下陸網(wǎng)站建設(shè)哪家好?成都創(chuàng)新互聯(lián)做網(wǎng)站,專注于網(wǎng)站建設(shè)十載,網(wǎng)設(shè)計領(lǐng)域的專業(yè)建站公司;建站業(yè)務(wù)涵蓋:下陸等地區(qū)。下陸做網(wǎng)站價格咨詢:18982081108
總體來說golang的map是hashmap,是使用數(shù)組+鏈表的形式實現(xiàn)的,使用拉鏈法消除hash沖突。
golang的map由兩種重要的結(jié)構(gòu),hmap和bmap(下文中都有解釋),主要就是hmap中包含一個指向bmap數(shù)組的指針,key經(jīng)過hash函數(shù)之后得到一個數(shù),這個數(shù)低位用于選擇bmap(當(dāng)作bmap數(shù)組指針的下表),高位用于放在bmap的[8]uint8數(shù)組中,用于快速試錯。然后一個bmap可以指向下一個bmap(拉鏈)。
Golang中map的底層實現(xiàn)是一個散列表,因此實現(xiàn)map的過程實際上就是實現(xiàn)散表的過程。在這個散列表中,主要出現(xiàn)的結(jié)構(gòu)體有兩個,一個叫 hmap (a header for a go map),一個叫 bmap (a bucket for a Go map,通常叫其bucket)。這兩種結(jié)構(gòu)的樣子分別如下所示:
hmap :
圖中有很多字段,但是便于理解map的架構(gòu),你只需要關(guān)心的只有一個,就是標(biāo)紅的字段: buckets數(shù)組 。Golang的map中用于存儲的結(jié)構(gòu)是bucket數(shù)組。而bucket(即bmap)的結(jié)構(gòu)是怎樣的呢?
bucket :
相比于hmap,bucket的結(jié)構(gòu)顯得簡單一些,標(biāo)紅的字段依然是“核心”,我們使用的map中的key和value就存儲在這里?!案呶还V怠睌?shù)組記錄的是當(dāng)前bucket中key相關(guān)的“索引”,稍后會詳細(xì)敘述。還有一個字段是一個指向擴(kuò)容后的bucket的指針,使得bucket會形成一個鏈表結(jié)構(gòu)。例如下圖:
由此看出hmap和bucket的關(guān)系是這樣的:
而bucket又是一個鏈表,所以,整體的結(jié)構(gòu)應(yīng)該是這樣的:
哈希表的特點是會有一個哈希函數(shù),對你傳來的key進(jìn)行哈希運算,得到唯一的值,一般情況下都是一個數(shù)值。Golang的map中也有這么一個哈希函數(shù),也會算出唯一的值,對于這個值的使用,Golang也是很有意思。
Golang把求得的值按照用途一分為二:高位和低位。
如圖所示,藍(lán)色為高位,紅色為低位。 然后低位用于尋找當(dāng)前key屬于hmap中的哪個bucket,而高位用于尋找bucket中的哪個key。上文中提到:bucket中有個屬性字段是“高位哈希值”數(shù)組,這里存的就是藍(lán)色的高位值,用來聲明當(dāng)前bucket中有哪些“key”,便于搜索查找。 需要特別指出的一點是:我們map中的key/value值都是存到同一個數(shù)組中的。數(shù)組中的順序是這樣的:
并不是key0/value0/key1/value1的形式,這樣做的好處是:在key和value的長度不同的時候,可 以消除padding(內(nèi)存對齊)帶來的空間浪費 。
現(xiàn)在,我們可以得到Go語言map的整個的結(jié)構(gòu)圖了:(hash結(jié)果的低位用于選擇把KV放在bmap數(shù)組中的哪一個bmap中,高位用于key的快速預(yù)覽,用于快速試錯)
map的擴(kuò)容
當(dāng)以上的哈希表增長的時候,Go語言會將bucket數(shù)組的數(shù)量擴(kuò)充一倍,產(chǎn)生一個新的bucket數(shù)組,并將舊數(shù)組的數(shù)據(jù)遷移至新數(shù)組。
加載因子
判斷擴(kuò)充的條件,就是哈希表中的加載因子(即loadFactor)。
加載因子是一個閾值,一般表示為:散列包含的元素數(shù) 除以 位置總數(shù)。是一種“產(chǎn)生沖突機(jī)會”和“空間使用”的平衡與折中:加載因子越小,說明空間空置率高,空間使用率小,但是加載因子越大,說明空間利用率上去了,但是“產(chǎn)生沖突機(jī)會”高了。
每種哈希表的都會有一個加載因子,數(shù)值超過加載因子就會為哈希表擴(kuò)容。
Golang的map的加載因子的公式是:map長度 / 2^B(這是代表bmap數(shù)組的長度,B是取的低位的位數(shù))閾值是6.5。其中B可以理解為已擴(kuò)容的次數(shù)。
當(dāng)Go的map長度增長到大于加載因子所需的map長度時,Go語言就會將產(chǎn)生一個新的bucket數(shù)組,然后把舊的bucket數(shù)組移到一個屬性字段oldbucket中。注意:并不是立刻把舊的數(shù)組中的元素轉(zhuǎn)義到新的bucket當(dāng)中,而是,只有當(dāng)訪問到具體的某個bucket的時候,會把bucket中的數(shù)據(jù)轉(zhuǎn)移到新的bucket中。
如下圖所示:當(dāng)擴(kuò)容的時候,Go的map結(jié)構(gòu)體中,會保存舊的數(shù)據(jù),和新生成的數(shù)組
上面部分代表舊的有數(shù)據(jù)的bucket,下面部分代表新生成的新的bucket。藍(lán)色代表存有數(shù)據(jù)的bucket,橘黃色代表空的bucket。
擴(kuò)容時map并不會立即把新數(shù)據(jù)做遷移,而是當(dāng)訪問原來舊bucket的數(shù)據(jù)的時候,才把舊數(shù)據(jù)做遷移,如下圖:
注意:這里并不會直接刪除舊的bucket,而是把原來的引用去掉,利用GC清除內(nèi)存。
map中數(shù)據(jù)的刪除
如果理解了map的整體結(jié)構(gòu),那么查找、更新、刪除的基本步驟應(yīng)該都很清楚了。這里不再贅述。
值得注意的是,找到了map中的數(shù)據(jù)之后,針對key和value分別做如下操作:
1
2
3
4
1、如果``key``是一個指針類型的,則直接將其置為空,等待GC清除;
2、如果是值類型的,則清除相關(guān)內(nèi)存。
3、同理,對``value``做相同的操作。
4、最后把key對應(yīng)的高位值對應(yīng)的數(shù)組index置為空。
前段時間在golang-China讀到這個貼:
個人覺得golang十分適合進(jìn)行網(wǎng)游服務(wù)器端開發(fā),寫下這篇文章總結(jié)一下。
從網(wǎng)游的角度看:
要成功的運營一款網(wǎng)游,很大程度上依賴于玩家自發(fā)形成的社區(qū)。只有玩家自發(fā)形成一個穩(wěn)定的生態(tài)系統(tǒng),游戲才能持續(xù)下去,避免鬼城的出現(xiàn)。而這就需要多次大量導(dǎo)入用戶,在同時在線用戶量達(dá)到某個臨界點的時候,才有可能完成。因此,多人同時在線十分有必要。
再來看網(wǎng)游的常見玩法,除了排行榜這類統(tǒng)計和數(shù)據(jù)匯總的功能外,基本沒有需要大量CPU時間的應(yīng)用。以前的項目里,即時戰(zhàn)斗產(chǎn)生的各種傷害計算對CPU的消耗也不大。玩家要完成一次操作,需要通過客戶端-服務(wù)器端-客戶端這樣一個來回,為了獲得高響應(yīng)速度,滿足玩家體驗,服務(wù)器端的處理也不能占用太多時間。所以,每次請求對應(yīng)的CPU占用是比較小的。
網(wǎng)游的IO主要分兩個方面,一個是網(wǎng)絡(luò)IO,一個是磁盤IO。網(wǎng)絡(luò)IO方面,可以分成美術(shù)資源的IO和游戲邏輯指令的IO,這里主要分析游戲邏輯的IO。游戲邏輯的IO跟CPU占用的情況相似,每次請求的字節(jié)數(shù)很小,但由于多人同時在線,因此并發(fā)數(shù)相當(dāng)高。另外,地圖信息的廣播也會帶來比較頻繁的網(wǎng)絡(luò)通信。磁盤IO方面,主要是游戲數(shù)據(jù)的保存。采用不同的數(shù)據(jù)庫,會有比較大的區(qū)別。以前的項目里,就經(jīng)歷了從MySQL轉(zhuǎn)向MongoDB這種內(nèi)存數(shù)據(jù)庫的過程,磁盤IO不再是瓶頸??傮w來說,還是用內(nèi)存做一級緩沖,避免大量小數(shù)據(jù)塊讀寫的方案。
針對網(wǎng)游的這些特點,golang的語言特性十分適合開發(fā)游戲服務(wù)器端。
首先,go語言提供goroutine機(jī)制作為原生的并發(fā)機(jī)制。每個goroutine所需的內(nèi)存很少,實際應(yīng)用中可以啟動大量的goroutine對并發(fā)連接進(jìn)行響應(yīng)。goroutine與gevent中的greenlet很相像,遇到IO阻塞的時候,調(diào)度器就會自動切換到另一個goroutine執(zhí)行,保證CPU不會因為IO而發(fā)生等待。而goroutine與gevent相比,沒有了python底層的GIL限制,就不需要利用多進(jìn)程來榨取多核機(jī)器的性能了。通過設(shè)置最大線程數(shù),可以控制go所啟動的線程,每個線程執(zhí)行一個goroutine,讓CPU滿負(fù)載運行。
同時,go語言為goroutine提供了獨到的通信機(jī)制channel。channel發(fā)生讀寫的時候,也會掛起當(dāng)前操作channel的goroutine,是一種同步阻塞通信。這樣既達(dá)到了通信的目的,又實現(xiàn)同步,用CSP模型的觀點看,并發(fā)模型就是通過一組進(jìn)程和進(jìn)程間的事件觸發(fā)解決任務(wù)的。雖然說,主流的編程語言之間,只要是圖靈完備的,他們就都能實現(xiàn)相同的功能。但go語言提供的這種協(xié)程間通信機(jī)制,十分優(yōu)雅地揭示了協(xié)程通信的本質(zhì),避免了以往鎖的顯式使用帶給程序員的心理負(fù)擔(dān),確是一大優(yōu)勢。進(jìn)行網(wǎng)游開發(fā)的程序員,可以將游戲邏輯按照單線程阻塞式的寫,不需要額外考慮線程調(diào)度的問題,以及線程間數(shù)據(jù)依賴的問題。因為,線程間的channel通信,已經(jīng)表達(dá)了線程間的數(shù)據(jù)依賴關(guān)系了,而go的調(diào)度器會給予妥善的處理。
另外,go語言提供的gc機(jī)制,以及對指針的保護(hù)式使用,可以大大減輕程序員的開發(fā)壓力,提高開發(fā)效率。
展望未來,我期待go語言社區(qū)能夠提供更多的goroutine間的隔離機(jī)制。個人十分推崇erlang社區(qū)的脆崩哲學(xué),推動應(yīng)用發(fā)生預(yù)期外行為時,盡早崩潰,再fork出新進(jìn)程處理新的請求。對于協(xié)程機(jī)制,需要由程序員保證執(zhí)行的函數(shù)不會發(fā)生死循環(huán),導(dǎo)致線程卡死。如果能夠定制goroutine所執(zhí)行函數(shù)的最大CPU執(zhí)行時間,及所能使用的最大內(nèi)存空間,對于提升系統(tǒng)的魯棒性,大有裨益。
Go語言也稱 Golang,兼具效率、性能、安全、健壯等特性。這套Go語言教程(Golang教程)通俗易懂,深入淺出,既適合沒有基礎(chǔ)的讀者快速入門,也適合工作多年的程序員查閱知識點。
Go 語言
這套教程在講解一些知識點時,將 Go 語言和其他多種語言進(jìn)行對比,讓掌握其它編程語言的讀者能迅速理解 Go 語言的特性。Go語言從底層原生支持并發(fā),無須第三方庫、開發(fā)者的編程技巧和開發(fā)經(jīng)驗就可以輕松搞定。
Go語言(或 Golang)起源于 2007 年,并在 2009 年正式對外發(fā)布。Go 是非常年輕的一門語言,它的主要目標(biāo)是“兼具 Python 等動態(tài)語言的開發(fā)速度和 C/C++ 等編譯型語言的性能與安全性”。
Go語言是編程語言設(shè)計的又一次嘗試,是對類C語言的重大改進(jìn),它不但能讓你訪問底層操作系統(tǒng),還提供了強(qiáng)大的網(wǎng)絡(luò)編程和并發(fā)編程支持。Go語言的用途眾多,可以進(jìn)行網(wǎng)絡(luò)編程、系統(tǒng)編程、并發(fā)編程、分布式編程。
Go語言的推出,旨在不損失應(yīng)用程序性能的情況下降低代碼的復(fù)雜性,具有“部署簡單、并發(fā)性好、語言設(shè)計良好、執(zhí)行性能好”等優(yōu)勢,目前國內(nèi)諸多 IT 公司均已采用Go語言開發(fā)項目。Go語言有時候被描述為“C 類似語言”,或者是“21 世紀(jì)的C語言”。Go 從C語言繼承了相似的表達(dá)式語法、控制流結(jié)構(gòu)、基礎(chǔ)數(shù)據(jù)類型、調(diào)用參數(shù)傳值、指針等很多思想,還有C語言一直所看中的編譯后機(jī)器碼的運行效率以及和現(xiàn)有操作系統(tǒng)的無縫適配。
因為Go語言沒有類和繼承的概念,所以它和 Java 或 C++ 看起來并不相同。但是它通過接口(interface)的概念來實現(xiàn)多態(tài)性。Go語言有一個清晰易懂的輕量級類型系統(tǒng),在類型之間也沒有層級之說。因此可以說Go語言是一門混合型的語言。
此外,很多重要的開源項目都是使用Go語言開發(fā)的,其中包括 Docker、Go-Ethereum、Thrraform 和 Kubernetes。Go 是編譯型語言,Go 使用編譯器來編譯代碼。編譯器將源代碼編譯成二進(jìn)制(或字節(jié)碼)格式;在編譯代碼時,編譯器檢查錯誤、優(yōu)化性能并輸出可在不同平臺上運行的二進(jìn)制文件。要創(chuàng)建并運行 Go 程序,程序員必須執(zhí)行如下步驟。
使用文本編輯器創(chuàng)建 Go 程序;
保存文件;編譯程序;運行編譯得到的可執(zhí)行文件。
這不同于 Python、Ruby 和 JavaScript 等語言,它們不包含編譯步驟。Go 自帶了編譯器,因此無須單獨安裝編譯器。
鏈喬教育在線旗下學(xué)碩創(chuàng)新區(qū)塊鏈技術(shù)工作站是中國教育部學(xué)校規(guī)劃建設(shè)發(fā)展中心開展的“智慧學(xué)習(xí)工場2020-學(xué)碩創(chuàng)新工作站 ”唯一獲準(zhǔn)的“區(qū)塊鏈技術(shù)專業(yè)”試點工作站。專業(yè)站立足為學(xué)生提供多樣化成長路徑,推進(jìn)專業(yè)學(xué)位研究生產(chǎn)學(xué)研結(jié)合培養(yǎng)模式改革,構(gòu)建應(yīng)用型、復(fù)合型人才培養(yǎng)體系。
Go語言中沒有“類”的概念,也不支持“類”的繼承等面向?qū)ο蟮母拍?。Go語言中通過結(jié)構(gòu)體的內(nèi)嵌再配合接口比面向?qū)ο缶哂懈叩臄U(kuò)展性和靈活性。
自定義類型
在Go語言中有一些基本的數(shù)據(jù)類型,如string、整型、浮點型、布爾等數(shù)據(jù)類型, Go語言中可以使用type關(guān)鍵字來定義自定義類型。
自定義類型是定義了一個全新的類型。我們可以基于內(nèi)置的基本類型定義,也可以通過struct定義。例如:
通過Type關(guān)鍵字的定義,MyInt就是一種新的類型,它具有int的特性。
類型別名
類型別名是Go1.9版本添加的新功能。
類型別名規(guī)定:TypeAlias只是Type的別名,本質(zhì)上TypeAlias與Type是同一個類型。就像一個孩子小時候有小名、乳名,上學(xué)后用學(xué)名,英語老師又會給他起英文名,但這些名字都指的是他本人。
type TypeAlias = Type
我們之前見過的rune和byte就是類型別名,他們的定義如下:
類型定義和類型別名的區(qū)別
類型別名與類型定義表面上看只有一個等號的差異,我們通過下面的這段代碼來理解它們之間的區(qū)別。
結(jié)果顯示a的類型是main.NewInt,表示main包下定義的NewInt類型。b的類型是int。MyInt類型只會在代碼中存在,編譯完成時并不會有MyInt類型。
Go語言中的基礎(chǔ)數(shù)據(jù)類型可以表示一些事物的基本屬性,但是當(dāng)我們想表達(dá)一個事物的全部或部分屬性時,這時候再用單一的基本數(shù)據(jù)類型明顯就無法滿足需求了,Go語言提供了一種自定義數(shù)據(jù)類型,可以封裝多個基本數(shù)據(jù)類型,這種數(shù)據(jù)類型叫結(jié)構(gòu)體,英文名稱struct。 也就是我們可以通過struct來定義自己的類型了。
Go語言中通過struct來實現(xiàn)面向?qū)ο蟆?/p>
結(jié)構(gòu)體的定義
使用type和struct關(guān)鍵字來定義結(jié)構(gòu)體,具體代碼格式如下:
其中:
舉個例子,我們定義一個Person(人)結(jié)構(gòu)體,代碼如下:
同樣類型的字段也可以寫在一行,
這樣我們就擁有了一個person的自定義類型,它有name、city、age三個字段,分別表示姓名、城市和年齡。這樣我們使用這個person結(jié)構(gòu)體就能夠很方便的在程序中表示和存儲人信息了。
語言內(nèi)置的基礎(chǔ)數(shù)據(jù)類型是用來描述一個值的,而結(jié)構(gòu)體是用來描述一組值的。比如一個人有名字、年齡和居住城市等,本質(zhì)上是一種聚合型的數(shù)據(jù)類型
結(jié)構(gòu)體實例化
只有當(dāng)結(jié)構(gòu)體實例化時,才會真正地分配內(nèi)存。也就是必須實例化后才能使用結(jié)構(gòu)體的字段。
基本實例化
舉個例子:
我們通過.來訪問結(jié)構(gòu)體的字段(成員變量),例如p1.name和p1.age等。
匿名結(jié)構(gòu)體
在定義一些臨時數(shù)據(jù)結(jié)構(gòu)等場景下還可以使用匿名結(jié)構(gòu)體。
創(chuàng)建指針類型結(jié)構(gòu)體
我們還可以通過使用new關(guān)鍵字對結(jié)構(gòu)體進(jìn)行實例化,得到的是結(jié)構(gòu)體的地址。 格式如下:
從打印的結(jié)果中我們可以看出p2是一個結(jié)構(gòu)體指針。
需要注意的是在Go語言中支持對結(jié)構(gòu)體指針直接使用.來訪問結(jié)構(gòu)體的成員。
取結(jié)構(gòu)體的地址實例化
使用對結(jié)構(gòu)體進(jìn)行取地址操作相當(dāng)于對該結(jié)構(gòu)體類型進(jìn)行了一次new實例化操作。
p3.name = "七米"其實在底層是(*p3).name = "七米",這是Go語言幫我們實現(xiàn)的語法糖。
結(jié)構(gòu)體初始化
沒有初始化的結(jié)構(gòu)體,其成員變量都是對應(yīng)其類型的零值。
使用鍵值對初始化
使用鍵值對對結(jié)構(gòu)體進(jìn)行初始化時,鍵對應(yīng)結(jié)構(gòu)體的字段,值對應(yīng)該字段的初始值。
也可以對結(jié)構(gòu)體指針進(jìn)行鍵值對初始化,例如:
當(dāng)某些字段沒有初始值的時候,該字段可以不寫。此時,沒有指定初始值的字段的值就是該字段類型的零值。
使用值的列表初始化
初始化結(jié)構(gòu)體的時候可以簡寫,也就是初始化的時候不寫鍵,直接寫值:
使用這種格式初始化時,需要注意:
結(jié)構(gòu)體內(nèi)存布局
結(jié)構(gòu)體占用一塊連續(xù)的內(nèi)存。
輸出:
【進(jìn)階知識點】關(guān)于Go語言中的內(nèi)存對齊推薦閱讀:在 Go 中恰到好處的內(nèi)存對齊
面試題
請問下面代碼的執(zhí)行結(jié)果是什么?
構(gòu)造函數(shù)
Go語言的結(jié)構(gòu)體沒有構(gòu)造函數(shù),我們可以自己實現(xiàn)。 例如,下方的代碼就實現(xiàn)了一個person的構(gòu)造函數(shù)。 因為struct是值類型,如果結(jié)構(gòu)體比較復(fù)雜的話,值拷貝性能開銷會比較大,所以該構(gòu)造函數(shù)返回的是結(jié)構(gòu)體指針類型。
調(diào)用構(gòu)造函數(shù)
方法和接收者
Go語言中的方法(Method)是一種作用于特定類型變量的函數(shù)。這種特定類型變量叫做接收者(Receiver)。接收者的概念就類似于其他語言中的this或者 self。
方法的定義格式如下:
其中,
舉個例子:
方法與函數(shù)的區(qū)別是,函數(shù)不屬于任何類型,方法屬于特定的類型。
指針類型的接收者
指針類型的接收者由一個結(jié)構(gòu)體的指針組成,由于指針的特性,調(diào)用方法時修改接收者指針的任意成員變量,在方法結(jié)束后,修改都是有效的。這種方式就十分接近于其他語言中面向?qū)ο笾械膖his或者self。 例如我們?yōu)镻erson添加一個SetAge方法,來修改實例變量的年齡。
調(diào)用該方法:
值類型的接收者
當(dāng)方法作用于值類型接收者時,Go語言會在代碼運行時將接收者的值復(fù)制一份。在值類型接收者的方法中可以獲取接收者的成員值,但修改操作只是針對副本,無法修改接收者變量本身。
什么時候應(yīng)該使用指針類型接收者
任意類型添加方法
在Go語言中,接收者的類型可以是任何類型,不僅僅是結(jié)構(gòu)體,任何類型都可以擁有方法。 舉個例子,我們基于內(nèi)置的int類型使用type關(guān)鍵字可以定義新的自定義類型,然后為我們的自定義類型添加方法。
注意事項: 非本地類型不能定義方法,也就是說我們不能給別的包的類型定義方法。
結(jié)構(gòu)體的匿名字段
匿名字段默認(rèn)采用類型名作為字段名,結(jié)構(gòu)體要求字段名稱必須唯一,因此一個結(jié)構(gòu)體中同種類型的匿名字段只能有一個。
嵌套結(jié)構(gòu)體
一個結(jié)構(gòu)體中可以嵌套包含另一個結(jié)構(gòu)體或結(jié)構(gòu)體指針。
嵌套匿名結(jié)構(gòu)體
當(dāng)訪問結(jié)構(gòu)體成員時會先在結(jié)構(gòu)體中查找該字段,找不到再去匿名結(jié)構(gòu)體中查找。
嵌套結(jié)構(gòu)體的字段名沖突
嵌套結(jié)構(gòu)體內(nèi)部可能存在相同的字段名。這個時候為了避免歧義需要指定具體的內(nèi)嵌結(jié)構(gòu)體的字段。
結(jié)構(gòu)體的“繼承”
Go語言中使用結(jié)構(gòu)體也可以實現(xiàn)其他編程語言中面向?qū)ο蟮睦^承。
結(jié)構(gòu)體字段的可見性
結(jié)構(gòu)體中字段大寫開頭表示可公開訪問,小寫表示私有(僅在定義當(dāng)前結(jié)構(gòu)體的包中可訪問)。
結(jié)構(gòu)體與JSON序列化
JSON(JavaScript Object Notation) 是一種輕量級的數(shù)據(jù)交換格式。易于人閱讀和編寫。同時也易于機(jī)器解析和生成。JSON鍵值對是用來保存JS對象的一種方式,鍵/值對組合中的鍵名寫在前面并用雙引號""包裹,使用冒號:分隔,然后緊接著值;多個鍵值之間使用英文,分隔。
結(jié)構(gòu)體標(biāo)簽(Tag)
Tag是結(jié)構(gòu)體的元信息,可以在運行的時候通過反射的機(jī)制讀取出來。 Tag在結(jié)構(gòu)體字段的后方定義,由一對反引號包裹起來,具體的格式如下:
`key1:"value1" key2:"value2"`
結(jié)構(gòu)體標(biāo)簽由一個或多個鍵值對組成。鍵與值使用冒號分隔,值用雙引號括起來。鍵值對之間使用一個空格分隔。 注意事項: 為結(jié)構(gòu)體編寫Tag時,必須嚴(yán)格遵守鍵值對的規(guī)則。結(jié)構(gòu)體標(biāo)簽的解析代碼的容錯能力很差,一旦格式寫錯,編譯和運行時都不會提示任何錯誤,通過反射也無法正確取值。例如不要在key和value之間添加空格。
例如我們?yōu)镾tudent結(jié)構(gòu)體的每個字段定義json序列化時使用的Tag:
Goroutine調(diào)度是一個很復(fù)雜的機(jī)制,下面嘗試用簡單的語言描述一下Goroutine調(diào)度機(jī)制,想要對其有更深入的了解可以去研讀一下源碼。
首先介紹一下GMP什么意思:
G ----------- goroutine: 即Go協(xié)程,每個go關(guān)鍵字都會創(chuàng)建一個協(xié)程。
M ---------- thread內(nèi)核級線程,所有的G都要放在M上才能運行。
P ----------- processor處理器,調(diào)度G到M上,其維護(hù)了一個隊列,存儲了所有需要它來調(diào)度的G。
Goroutine 調(diào)度器P和 OS 調(diào)度器是通過 M 結(jié)合起來的,每個 M 都代表了 1 個內(nèi)核線程,OS 調(diào)度器負(fù)責(zé)把內(nèi)核線程分配到 CPU 的核上執(zhí)行
模型圖:
避免頻繁的創(chuàng)建、銷毀線程,而是對線程的復(fù)用。
1)work stealing機(jī)制
當(dāng)本線程無可運行的G時,嘗試從其他線程綁定的P偷取G,而不是銷毀線程。
2)hand off機(jī)制
當(dāng)本線程M0因為G0進(jìn)行系統(tǒng)調(diào)用阻塞時,線程釋放綁定的P,把P轉(zhuǎn)移給其他空閑的線程執(zhí)行。進(jìn)而某個空閑的M1獲取P,繼續(xù)執(zhí)行P隊列中剩下的G。而M0由于陷入系統(tǒng)調(diào)用而進(jìn)被阻塞,M1接替M0的工作,只要P不空閑,就可以保證充分利用CPU。M1的來源有可能是M的緩存池,也可能是新建的。當(dāng)G0系統(tǒng)調(diào)用結(jié)束后,根據(jù)M0是否能獲取到P,將會將G0做不同的處理:
如果有空閑的P,則獲取一個P,繼續(xù)執(zhí)行G0。
如果沒有空閑的P,則將G0放入全局隊列,等待被其他的P調(diào)度。然后M0將進(jìn)入緩存池睡眠。
如下圖
GOMAXPROCS設(shè)置P的數(shù)量,最多有GOMAXPROCS個線程分布在多個CPU上同時運行
在Go中一個goroutine最多占用CPU 10ms,防止其他goroutine被餓死。
具體可以去看另一篇文章
【Golang詳解】go語言調(diào)度機(jī)制 搶占式調(diào)度
當(dāng)創(chuàng)建一個新的G之后優(yōu)先加入本地隊列,如果本地隊列滿了,會將本地隊列的G移動到全局隊列里面,當(dāng)M執(zhí)行work stealing從其他P偷不到G時,它可以從全局G隊列獲取G。
協(xié)程經(jīng)歷過程
我們創(chuàng)建一個協(xié)程 go func()經(jīng)歷過程如下圖:
說明:
這里有兩個存儲G的隊列,一個是局部調(diào)度器P的本地隊列、一個是全局G隊列。新創(chuàng)建的G會先保存在P的本地隊列中,如果P的本地隊列已經(jīng)滿了就會保存在全局的隊列中;處理器本地隊列是一個使用數(shù)組構(gòu)成的環(huán)形鏈表,它最多可以存儲 256 個待執(zhí)行任務(wù)。
G只能運行在M中,一個M必須持有一個P,M與P是1:1的關(guān)系。M會從P的本地隊列彈出一個可執(zhí)行狀態(tài)的G來執(zhí)行,如果P的本地隊列為空,就會想其他的MP組合偷取一個可執(zhí)行的G來執(zhí)行;
一個M調(diào)度G執(zhí)行的過程是一個循環(huán)機(jī)制;會一直從本地隊列或全局隊列中獲取G
上面說到P的個數(shù)默認(rèn)等于CPU核數(shù),每個M必須持有一個P才可以執(zhí)行G,一般情況下M的個數(shù)會略大于P的個數(shù),這多出來的M將會在G產(chǎn)生系統(tǒng)調(diào)用時發(fā)揮作用。類似線程池,Go也提供一個M的池子,需要時從池子中獲取,用完放回池子,不夠用時就再創(chuàng)建一個。
work-stealing調(diào)度算法:當(dāng)M執(zhí)行完了當(dāng)前P的本地隊列隊列里的所有G后,P也不會就這么在那躺尸啥都不干,它會先嘗試從全局隊列隊列尋找G來執(zhí)行,如果全局隊列為空,它會隨機(jī)挑選另外一個P,從它的隊列里中拿走一半的G到自己的隊列中執(zhí)行。
如果一切正常,調(diào)度器會以上述的那種方式順暢地運行,但這個世界沒這么美好,總有意外發(fā)生,以下分析goroutine在兩種例外情況下的行為。
Go runtime會在下面的goroutine被阻塞的情況下運行另外一個goroutine:
用戶態(tài)阻塞/喚醒
當(dāng)goroutine因為channel操作或者network I/O而阻塞時(實際上golang已經(jīng)用netpoller實現(xiàn)了goroutine網(wǎng)絡(luò)I/O阻塞不會導(dǎo)致M被阻塞,僅阻塞G,這里僅僅是舉個栗子),對應(yīng)的G會被放置到某個wait隊列(如channel的waitq),該G的狀態(tài)由_Gruning變?yōu)開Gwaitting,而M會跳過該G嘗試獲取并執(zhí)行下一個G,如果此時沒有可運行的G供M運行,那么M將解綁P,并進(jìn)入sleep狀態(tài);當(dāng)阻塞的G被另一端的G2喚醒時(比如channel的可讀/寫通知),G被標(biāo)記為,嘗試加入G2所在P的runnext(runnext是線程下一個需要執(zhí)行的 Goroutine。), 然后再是P的本地隊列和全局隊列。
系統(tǒng)調(diào)用阻塞
當(dāng)M執(zhí)行某一個G時候如果發(fā)生了阻塞操作,M會阻塞,如果當(dāng)前有一些G在執(zhí)行,調(diào)度器會把這個線程M從P中摘除,然后再創(chuàng)建一個新的操作系統(tǒng)的線程(如果有空閑的線程可用就復(fù)用空閑線程)來服務(wù)于這個P。當(dāng)M系統(tǒng)調(diào)用結(jié)束時候,這個G會嘗試獲取一個空閑的P執(zhí)行,并放入到這個P的本地隊列。如果獲取不到P,那么這個線程M變成休眠狀態(tài), 加入到空閑線程中,然后這個G會被放入全局隊列中。
隊列輪轉(zhuǎn)
可見每個P維護(hù)著一個包含G的隊列,不考慮G進(jìn)入系統(tǒng)調(diào)用或IO操作的情況下,P周期性的將G調(diào)度到M中執(zhí)行,執(zhí)行一小段時間,將上下文保存下來,然后將G放到隊列尾部,然后從隊列中重新取出一個G進(jìn)行調(diào)度。
除了每個P維護(hù)的G隊列以外,還有一個全局的隊列,每個P會周期性地查看全局隊列中是否有G待運行并將其調(diào)度到M中執(zhí)行,全局隊列中G的來源,主要有從系統(tǒng)調(diào)用中恢復(fù)的G。之所以P會周期性地查看全局隊列,也是為了防止全局隊列中的G被餓死。
除了每個P維護(hù)的G隊列以外,還有一個全局的隊列,每個P會周期性地查看全局隊列中是否有G待運行并將其調(diào)度到M中執(zhí)行,全局隊列中G的來源,主要有從系統(tǒng)調(diào)用中恢復(fù)的G。之所以P會周期性地查看全局隊列,也是為了防止全局隊列中的G被餓死。
M0
M0是啟動程序后的編號為0的主線程,這個M對應(yīng)的實例會在全局變量rutime.m0中,不需要在heap上分配,M0負(fù)責(zé)執(zhí)行初始化操作和啟動第一個G,在之后M0就和其他的M一樣了
G0
G0是每次啟動一個M都會第一個創(chuàng)建的goroutine,G0僅用于負(fù)責(zé)調(diào)度G,G0不指向任何可執(zhí)行的函數(shù),每個M都會有一個自己的G0,在調(diào)度或系統(tǒng)調(diào)用時會使用G0的棧空間,全局變量的G0是M0的G0
一個G由于調(diào)度被中斷,此后如何恢復(fù)?
中斷的時候?qū)⒓拇嫫骼锏臈P畔?,保存到自己的G對象里面。當(dāng)再次輪到自己執(zhí)行時,將自己保存的棧信息復(fù)制到寄存器里面,這樣就接著上次之后運行了。
我這里只是根據(jù)自己的理解進(jìn)行了簡單的介紹,想要詳細(xì)了解有關(guān)GMP的底層原理可以去看Go調(diào)度器 G-P-M 模型的設(shè)計者的文檔或直接看源碼
參考: ()
()
作為C語言家族的一員,go和c一樣也支持結(jié)構(gòu)體??梢灶惐扔趈ava的一個POJO。
在學(xué)習(xí)定義結(jié)構(gòu)體之前,先學(xué)習(xí)下定義一個新類型。
新類型 T1 是基于 Go 原生類型 int 定義的新自定義類型,而新類型 T2 則是 基于剛剛定義的類型 T1,定義的新類型。
這里要引入一個底層類型的概念。
如果一個新類型是基于某個 Go 原生類型定義的, 那么我們就叫 Go 原生類型為新類型的底層類型
在上面的例子中,int就是T1的底層類型。
但是T1不是T2的底層類型,只有原生類型才可以作為底層類型,所以T2的底層類型還是int
底層類型是很重要的,因為對兩個變量進(jìn)行顯式的類型轉(zhuǎn)換,只有底層類型相同的變量間才能相互轉(zhuǎn)換。底層類型是判斷兩個類型本質(zhì)上是否相同的根本。
這種類型定義方式通常用在 項目的漸進(jìn)式重構(gòu),還有對已有包的二次封裝方面
類型別名表示新類型和原類型完全等價,實際上就是同一種類型。只不過名字不同而已。
一般我們都是定義一個有名的結(jié)構(gòu)體。
字段名的大小寫決定了字段是否包外可用。只有大寫的字段可以被包外引用。
還有一個點提一下
如果換行來寫
Age: 66,后面這個都好不能省略
還有一個點,觀察e3的賦值
new返回的是一個指針。然后指針可以直接點號賦值。這說明go默認(rèn)進(jìn)行了取值操作
e3.Age 等價于 (*e3).Age
如上定義了一個空的結(jié)構(gòu)體Empty。打印了元素e的內(nèi)存大小是0。
有什么用呢?
基于空結(jié)構(gòu)體類型內(nèi)存零開銷這樣的特性,我們在日常 Go 開發(fā)中會經(jīng)常使用空 結(jié)構(gòu)體類型元素,作為一種“事件”信息進(jìn)行 Goroutine 之間的通信
這種以空結(jié)構(gòu)體為元素類建立的 channel,是目前能實現(xiàn)的、內(nèi)存占用最小的 Goroutine 間通信方式。
這種形式需要說的是幾個語法糖。
語法糖1:
對于結(jié)構(gòu)體字段,可以省略字段名,只寫結(jié)構(gòu)體名。默認(rèn)字段名就是結(jié)構(gòu)體名
這種方式稱為 嵌入字段
語法糖2:
如果是以嵌入字段形式寫的結(jié)構(gòu)體
可以省略嵌入的Reader字段,而直接訪問ReaderName
此時book是一個各個屬性全是對應(yīng)類型零值的一個實例。不是nil。這種情況在Go中稱為零值可用。不像java會導(dǎo)致npe
結(jié)構(gòu)體定義時可以在字段后面追加標(biāo)簽說明。
tag的格式為反單引號
tag的作用是可以使用[反射]來檢視字段的標(biāo)簽信息。
具體的作用還要看使用的場景。
比如這里的tag是為了幫助 encoding/json 標(biāo)準(zhǔn)包在解析對象時可以利用的規(guī)則。比如omitempty表示該字段沒有值就不打印出來。