string是Go語言中的基礎(chǔ)數(shù)據(jù)類型。
我們提供的服務(wù)有:網(wǎng)站制作、網(wǎng)站建設(shè)、微信公眾號開發(fā)、網(wǎng)站優(yōu)化、網(wǎng)站認(rèn)證、仲巴ssl等。為上千企事業(yè)單位解決了網(wǎng)站和推廣的問題。提供周到的售前咨詢和貼心的售后服務(wù),是有科學(xué)管理、有技術(shù)的仲巴網(wǎng)站制作公司
聲明string變量非常簡單,常見的方式有以下兩種:
聲明一個(gè)空字符串后再賦值。
var s string。
s = "hello world"。
需要注意的是空字符只是長度為0,但不是nil。不存在值為nil的string。
使用簡短變量聲明:
s := "hello world" //直接初始化字符串。
雙引號與單引號。
字符串不僅可以使用雙引號賦值,也可以使用反單引號賦值,它們的區(qū)別是在于對特殊字符的處理。
假如我們希望string變量表示下面的字符串,它包括換行符和雙引號:
Hi。
this is "Steven"。
1。
2。
使用雙引號表示時(shí),需要對特殊字符轉(zhuǎn)義,如下所示:
s:= "Hi, \nthis is \"Steven\"."。
1。
如果使用反單引號時(shí),不需要對特殊符號轉(zhuǎn)義,如下所示:
s := Hi。
this is "Steven"。
需要注意的是,字符串拼接會(huì)觸發(fā)內(nèi)存分配以及內(nèi)存拷貝,單行語句拼接多個(gè)字符串只分配一次內(nèi)存。比如上面的語句中,在拼接時(shí),會(huì)先計(jì)算最終字符串的長度后再分配內(nèi)存。
類型轉(zhuǎn)換:
項(xiàng)目中,數(shù)據(jù)經(jīng)常需要在string和字節(jié)[]byte之間轉(zhuǎn)換。
云和安全管理服務(wù)專家新鈦云服 張春翻譯
這種方法有幾個(gè)缺點(diǎn)。首先,它可以對程序員隱藏錯(cuò)誤處理路徑,特別是在捕獲異常不是強(qiáng)制性的情況下,例如在 Python 中。即使在具有必須處理的 Java 風(fēng)格的檢查異常的語言中,如果在與原始調(diào)用不同的級別上處理錯(cuò)誤,也并不總是很明顯錯(cuò)誤是從哪里引發(fā)的。
我們都見過長長的代碼塊包裝在一個(gè) try-catch 塊中。在這種情況下,catch 塊實(shí)際上充當(dāng) goto 語句,這通常被認(rèn)為是有害的(奇怪的是,C 中的關(guān)鍵字被認(rèn)為可以接受的少數(shù)用例之一是錯(cuò)誤后清理,因?yàn)樵撜Z言沒有 Golang- 樣式延遲語句)。
如果你確實(shí)從源頭捕獲異常,你會(huì)得到一個(gè)不太優(yōu)雅的 Go 錯(cuò)誤模式版本。這可能會(huì)解決混淆代碼的問題,但會(huì)遇到另一個(gè)問題:性能。在諸如 Java 之類的語言中,拋出異??赡鼙群瘮?shù)的常規(guī)返回慢數(shù)百倍。
Java 中最大的性能成本是由打印異常的堆棧跟蹤造成的,這是昂貴的,因?yàn)檫\(yùn)行的程序必須檢查編譯它的源代碼 。僅僅進(jìn)入一個(gè) try 塊也不是空閑的,因?yàn)樾枰4?CPU 內(nèi)存寄存器的先前狀態(tài),因?yàn)樗鼈兛赡苄枰趻伋霎惓5那闆r下恢復(fù)。
如果您將異常視為通常不會(huì)發(fā)生的異常情況,那么異常的缺點(diǎn)并不重要。這可能是傳統(tǒng)的單體應(yīng)用程序的情況,其中大部分代碼庫不必進(jìn)行網(wǎng)絡(luò)調(diào)用——一個(gè)操作格式良好的數(shù)據(jù)的函數(shù)不太可能遇到錯(cuò)誤(除了錯(cuò)誤的情況)。一旦您在代碼中添加 I/O,無錯(cuò)誤代碼的夢想就會(huì)破滅:您可以忽略錯(cuò)誤,但不能假裝它們不存在!
try {
doSometing()
} catch (IOException e) {
// ignore it
}
與大多數(shù)其他編程語言不同,Golang 接受錯(cuò)誤是不可避免的。 如果在單體架構(gòu)時(shí)代還不是這樣,那么在今天的模塊化后端服務(wù)中,服務(wù)通常和外部 API 調(diào)用、數(shù)據(jù)庫讀取和寫入以及與其他服務(wù)通信 。
以上所有方法都可能失敗,解析或驗(yàn)證從它們接收到的數(shù)據(jù)(通常在無模式 JSON 中)也可能失敗。Golang 使可以從這些調(diào)用返回的錯(cuò)誤顯式化,與普通返回值的等級相同。從函數(shù)調(diào)用返回多個(gè)值的能力支持這一點(diǎn),這在大多數(shù)語言中通常是不可能的。Golang 的錯(cuò)誤處理系統(tǒng)不僅僅是一種語言怪癖,它是一種將錯(cuò)誤視為替代返回值的完全不同的方式!
重復(fù) if err != nil
對 Go 錯(cuò)誤處理的一個(gè)常見批評是被迫重復(fù)以下代碼塊:
res, err := doSomething()
if err != nil {
// Handle error
}
對于新用戶來說,這可能會(huì)覺得沒用而且浪費(fèi)行數(shù):在其他語言中需要 3 行的函數(shù)很可能會(huì)增長到 12 行 :
這么多行代碼!這么低效!如果您認(rèn)為上述內(nèi)容不優(yōu)雅或浪費(fèi)代碼,您可能忽略了我們檢查代碼中的錯(cuò)誤的全部原因:我們需要能夠以不同的方式處理它們!對 API 或數(shù)據(jù)庫的調(diào)用可能會(huì)被重試。
有時(shí)事件的順序很重要:調(diào)用外部 API 之前發(fā)生的錯(cuò)誤可能不是什么大問題(因?yàn)閿?shù)據(jù)從未通過發(fā)送),而 API 調(diào)用和寫入本地?cái)?shù)據(jù)庫之間的錯(cuò)誤可能需要立即注意,因?yàn)?這可能意味著系統(tǒng)最終處于不一致的狀態(tài)。即使我們只想將錯(cuò)誤傳播給調(diào)用者,我們也可能希望用失敗的解釋來包裝它們,或者為每個(gè)錯(cuò)誤返回一個(gè)自定義錯(cuò)誤類型。
并非所有錯(cuò)誤都是相同的,并且向調(diào)用者返回適當(dāng)?shù)腻e(cuò)誤是 API 設(shè)計(jì)的重要部分,無論是對于內(nèi)部包還是 REST API 。
不必?fù)?dān)心在你的代碼中重復(fù) if err != nil ——這就是 Go 中的代碼應(yīng)該看起來的樣子。
自定義錯(cuò)誤類型和錯(cuò)誤包裝
從導(dǎo)出的方法返回錯(cuò)誤時(shí),請考慮指定自定義錯(cuò)誤類型,而不是單獨(dú)使用錯(cuò)誤字符串。字符串在意外代碼中是可以的,但在導(dǎo)出的函數(shù)中,它們成為函數(shù)公共 API 的一部分。更改錯(cuò)誤字符串將是一項(xiàng)重大更改——如果沒有明確的錯(cuò)誤類型,需要檢查返回錯(cuò)誤類型的單元測試將不得不依賴原始字符串值!事實(shí)上,基于字符串的錯(cuò)誤也使得在私有方法中測試不同的錯(cuò)誤案例變得困難,因此您也應(yīng)該考慮在包中使用它們。回到錯(cuò)誤與異常的爭論,返回錯(cuò)誤也使代碼比拋出異常更容易測試,因?yàn)殄e(cuò)誤只是要檢查的返回值。不需要測試框架或在測試中捕獲異常 。
可以在 database/sql 包中找到簡單自定義錯(cuò)誤類型的一個(gè)很好的示例。它定義了一個(gè)導(dǎo)出常量列表,表示包可以返回的錯(cuò)誤類型,最著名的是 sql.ErrNoRows。雖然從 API 設(shè)計(jì)的角度來看,這種特定的錯(cuò)誤類型有點(diǎn)問題(您可能會(huì)爭辯說 API 應(yīng)該返回一個(gè)空結(jié)構(gòu)而不是錯(cuò)誤),但任何需要檢查空行的應(yīng)用程序都可以導(dǎo)入該常量并在代碼中使用它不必?fù)?dān)心錯(cuò)誤消息本身會(huì)改變和破壞代碼。
對于更復(fù)雜的錯(cuò)誤處理,您可以通過實(shí)現(xiàn)返回錯(cuò)誤字符串的 Error() 方法來定義自定義錯(cuò)誤類型。自定義錯(cuò)誤可以包括元數(shù)據(jù),例如錯(cuò)誤代碼或原始請求參數(shù)。如果您想表示錯(cuò)誤類別,它們很有用。DigitalOcean 的本教程展示了如何使用自定義錯(cuò)誤類型來表示可以重試的一類臨時(shí)錯(cuò)誤。
通常,錯(cuò)誤會(huì)通過將低級錯(cuò)誤與更高級別的解釋包裝起來,從而在程序的調(diào)用堆棧中傳播。例如,數(shù)據(jù)庫錯(cuò)誤可能會(huì)以下列格式記錄在 API 調(diào)用處理程序中:調(diào)用 CreateUser 端點(diǎn)時(shí)出錯(cuò):查詢數(shù)據(jù)庫時(shí)出錯(cuò):pq:檢測到死鎖。這很有用,因?yàn)樗梢詭椭覀兏欏e(cuò)誤在系統(tǒng)中傳播的過程,向我們展示根本原因(數(shù)據(jù)庫事務(wù)引擎中的死鎖)以及它對更廣泛系統(tǒng)的影響(調(diào)用者無法創(chuàng)建新用戶)。
自 Go 1.13 以來,此模式具有特殊的語言支持,并帶有錯(cuò)誤包裝。通過在創(chuàng)建字符串錯(cuò)誤時(shí)使用 %w 動(dòng)詞,可以使用 Unwrap() 方法訪問底層錯(cuò)誤。除了比較錯(cuò)誤相等性的函數(shù) errors.Is() 和 errors.As() 外,程序還可以獲取包裝錯(cuò)誤的原始類型或標(biāo)識(shí)。這在某些情況下可能很有用,盡管我認(rèn)為在確定如何處理所述錯(cuò)誤時(shí)最好使用頂級錯(cuò)誤的類型。
Panics
不要 panic()!長時(shí)間運(yùn)行的應(yīng)用程序應(yīng)該優(yōu)雅地處理錯(cuò)誤而不是panic。即使在無法恢復(fù)的情況下(例如在啟動(dòng)時(shí)驗(yàn)證配置),最好記錄一個(gè)錯(cuò)誤并優(yōu)雅地退出。panic比錯(cuò)誤消息更難診斷,并且可能會(huì)跳過被推遲的重要關(guān)閉代碼。
Logging
我還想簡要介紹一下日志記錄,因?yàn)樗翘幚礤e(cuò)誤的關(guān)鍵部分。通常你能做的最好的事情就是記錄收到的錯(cuò)誤并繼續(xù)下一個(gè)請求。
除非您正在構(gòu)建簡單的命令行工具或個(gè)人項(xiàng)目,否則您的應(yīng)用程序應(yīng)該使用結(jié)構(gòu)化的日志庫,該庫可以為日志添加時(shí)間戳,并提供對日志級別的控制。最后一部分特別重要,因?yàn)樗鼘⒃试S您突出顯示應(yīng)用程序記錄的所有錯(cuò)誤和警告。通過幫助將它們與信息級日志分開,這將為您節(jié)省無數(shù)時(shí)間。
微服務(wù)架構(gòu)還應(yīng)該在日志行中包含服務(wù)的名稱以及機(jī)器實(shí)例的名稱。默認(rèn)情況下記錄這些時(shí),程序代碼不必?fù)?dān)心包含它們。您也可以在日志的結(jié)構(gòu)化部分中記錄其他字段,例如收到的錯(cuò)誤(如果您不想將其嵌入日志消息本身)或有問題的請求或響應(yīng)。只需確保您的日志沒有泄露任何敏感數(shù)據(jù),例如密碼、API 密鑰或用戶的個(gè)人數(shù)據(jù)!
對于日志庫,我過去使用過 logrus 和 zerolog,但您也可以選擇其他結(jié)構(gòu)化日志庫。如果您想了解更多信息,互聯(lián)網(wǎng)上有許多關(guān)于如何使用這些的指南。如果您將應(yīng)用程序部署到云中,您可能需要日志庫上的適配器來根據(jù)您的云平臺(tái)的日志 API 格式化日志 - 沒有它,云平臺(tái)可能無法檢測到日志級別等某些功能。
如果您在應(yīng)用程序中使用調(diào)試級別日志(默認(rèn)情況下通常不記錄),請確保您的應(yīng)用程序可以輕松更改日志級別,而無需更改代碼。更改日志級別還可以暫時(shí)使信息級別甚至警告級別的日志靜音,以防它們突然變得過于嘈雜并開始淹沒錯(cuò)誤。您可以使用在啟動(dòng)時(shí)檢查以設(shè)置日志級別的環(huán)境變量來實(shí)現(xiàn)這一點(diǎn)。
原文:
本文目錄如下,閱讀本文后,將一網(wǎng)打盡下面Golang Map相關(guān)面試題
Go中的map是一個(gè)指針,占用8個(gè)字節(jié),指向hmap結(jié)構(gòu)體; 源碼 src/runtime/map.go 中可以看到map的底層結(jié)構(gòu)
每個(gè)map的底層結(jié)構(gòu)是hmap,hmap包含若干個(gè)結(jié)構(gòu)為bmap的bucket數(shù)組。每個(gè)bucket底層都采用鏈表結(jié)構(gòu)。接下來,我們來詳細(xì)看下map的結(jié)構(gòu)
bmap 就是我們常說的“桶”,一個(gè)桶里面會(huì)最多裝 8 個(gè) key,這些 key 之所以會(huì)落入同一個(gè)桶,是因?yàn)樗鼈兘?jīng)過哈希計(jì)算后,哈希結(jié)果是“一類”的,關(guān)于key的定位我們在map的查詢和插入中詳細(xì)說明。在桶內(nèi),又會(huì)根據(jù) key 計(jì)算出來的 hash 值的高 8 位來決定 key 到底落入桶內(nèi)的哪個(gè)位置(一個(gè)桶內(nèi)最多有8個(gè)位置)。
bucket內(nèi)存數(shù)據(jù)結(jié)構(gòu)可視化如下:
注意到 key 和 value 是各自放在一起的,并不是 key/value/key/value/... 這樣的形式。源碼里說明這樣的好處是在某些情況下可以省略掉 padding字段,節(jié)省內(nèi)存空間。
當(dāng) map 的 key 和 value 都不是指針,并且 size 都小于 128 字節(jié)的情況下,會(huì)把 bmap 標(biāo)記為不含指針,這樣可以避免 gc 時(shí)掃描整個(gè) hmap。但是,我們看 bmap 其實(shí)有一個(gè) overflow 的字段,是指針類型的,破壞了 bmap 不含指針的設(shè)想,這時(shí)會(huì)把 overflow 移動(dòng)到 extra 字段來。
map是個(gè)指針,底層指向hmap,所以是個(gè)引用類型
golang 有三個(gè)常用的高級類型 slice 、map、channel, 它們都是 引用類型 ,當(dāng)引用類型作為函數(shù)參數(shù)時(shí),可能會(huì)修改原內(nèi)容數(shù)據(jù)。
golang 中沒有引用傳遞,只有值和指針傳遞。所以 map 作為函數(shù)實(shí)參傳遞時(shí)本質(zhì)上也是值傳遞,只不過因?yàn)?map 底層數(shù)據(jù)結(jié)構(gòu)是通過指針指向?qū)嶋H的元素存儲(chǔ)空間,在被調(diào)函數(shù)中修改 map,對調(diào)用者同樣可見,所以 map 作為函數(shù)實(shí)參傳遞時(shí)表現(xiàn)出了引用傳遞的效果。
因此,傳遞 map 時(shí),如果想修改map的內(nèi)容而不是map本身,函數(shù)形參無需使用指針
map 底層數(shù)據(jù)結(jié)構(gòu)是通過指針指向?qū)嶋H的元素 存儲(chǔ)空間 ,這種情況下,對其中一個(gè)map的更改,會(huì)影響到其他map
map 在沒有被修改的情況下,使用 range 多次遍歷 map 時(shí)輸出的 key 和 value 的順序可能不同。這是 Go 語言的設(shè)計(jì)者們有意為之,在每次 range 時(shí)的順序被隨機(jī)化,旨在提示開發(fā)者們,Go 底層實(shí)現(xiàn)并不保證 map 遍歷順序穩(wěn)定,請大家不要依賴 range 遍歷結(jié)果順序。
map 本身是無序的,且遍歷時(shí)順序還會(huì)被隨機(jī)化,如果想順序遍歷 map,需要對 map key 先排序,再按照 key 的順序遍歷 map。
map默認(rèn)是并發(fā)不安全的,原因如下:
Go 官方在經(jīng)過了長時(shí)間的討論后,認(rèn)為 Go map 更應(yīng)適配典型使用場景(不需要從多個(gè) goroutine 中進(jìn)行安全訪問),而不是為了小部分情況(并發(fā)訪問),導(dǎo)致大部分程序付出加鎖代價(jià)(性能),決定了不支持。
場景: 2個(gè)協(xié)程同時(shí)讀和寫,以下程序會(huì)出現(xiàn)致命錯(cuò)誤:fatal error: concurrent map writes
如果想實(shí)現(xiàn)map線程安全,有兩種方式:
方式一:使用讀寫鎖 map + sync.RWMutex
方式二:使用golang提供的 sync.Map
sync.map是用讀寫分離實(shí)現(xiàn)的,其思想是空間換時(shí)間。和map+RWLock的實(shí)現(xiàn)方式相比,它做了一些優(yōu)化:可以無鎖訪問read map,而且會(huì)優(yōu)先操作read map,倘若只操作read map就可以滿足要求(增刪改查遍歷),那就不用去操作write map(它的讀寫都要加鎖),所以在某些特定場景中它發(fā)生鎖競爭的頻率會(huì)遠(yuǎn)遠(yuǎn)小于map+RWLock的實(shí)現(xiàn)方式。
golang中map是一個(gè)kv對集合。底層使用hash table,用鏈表來解決沖突 ,出現(xiàn)沖突時(shí),不是每一個(gè)key都申請一個(gè)結(jié)構(gòu)通過鏈表串起來,而是以bmap為最小粒度掛載,一個(gè)bmap可以放8個(gè)kv。在哈希函數(shù)的選擇上,會(huì)在程序啟動(dòng)時(shí),檢測 cpu 是否支持 aes,如果支持,則使用 aes hash,否則使用 memhash。
map有3鐘初始化方式,一般通過make方式創(chuàng)建
map的創(chuàng)建通過生成匯編碼可以知道,make創(chuàng)建map時(shí)調(diào)用的底層函數(shù)是 runtime.makemap 。如果你的map初始容量小于等于8會(huì)發(fā)現(xiàn)走的是 runtime.fastrand 是因?yàn)槿萘啃∮?時(shí)不需要生成多個(gè)桶,一個(gè)桶的容量就可以滿足
makemap函數(shù)會(huì)通過 fastrand 創(chuàng)建一個(gè)隨機(jī)的哈希種子,然后根據(jù)傳入的 hint 計(jì)算出需要的最小需要的桶的數(shù)量,最后再使用 makeBucketArray 創(chuàng)建用于保存桶的數(shù)組,這個(gè)方法其實(shí)就是根據(jù)傳入的 B 計(jì)算出的需要?jiǎng)?chuàng)建的桶數(shù)量在內(nèi)存中分配一片連續(xù)的空間用于存儲(chǔ)數(shù)據(jù),在創(chuàng)建桶的過程中還會(huì)額外創(chuàng)建一些用于保存溢出數(shù)據(jù)的桶,數(shù)量是 2^(B-4) 個(gè)。初始化完成返回hmap指針。
找到一個(gè) B,使得 map 的裝載因子在正常范圍內(nèi)
Go 語言中讀取 map 有兩種語法:帶 comma 和 不帶 comma。當(dāng)要查詢的 key 不在 map 里,帶 comma 的用法會(huì)返回一個(gè) bool 型變量提示 key 是否在 map 中;而不帶 comma 的語句則會(huì)返回一個(gè) value 類型的零值。如果 value 是 int 型就會(huì)返回 0,如果 value 是 string 類型,就會(huì)返回空字符串。
map的查找通過生成匯編碼可以知道,根據(jù) key 的不同類型,編譯器會(huì)將查找函數(shù)用更具體的函數(shù)替換,以優(yōu)化效率:
函數(shù)首先會(huì)檢查 map 的標(biāo)志位 flags。如果 flags 的寫標(biāo)志位此時(shí)被置 1 了,說明有其他協(xié)程在執(zhí)行“寫”操作,進(jìn)而導(dǎo)致程序 panic。這也說明了 map 對協(xié)程是不安全的。
key經(jīng)過哈希函數(shù)計(jì)算后,得到的哈希值如下(主流64位機(jī)下共 64 個(gè) bit 位):
m: 桶的個(gè)數(shù)
從buckets 通過 hash m 得到對應(yīng)的bucket,如果bucket正在擴(kuò)容,并且沒有擴(kuò)容完成,則從oldbuckets得到對應(yīng)的bucket
計(jì)算hash所在桶編號:
用上一步哈希值最后的 5 個(gè) bit 位,也就是 01010 ,值為 10,也就是 10 號桶(范圍是0~31號桶)
計(jì)算hash所在的槽位:
用上一步哈希值哈希值的高8個(gè)bit 位,也就是 10010111 ,轉(zhuǎn)化為十進(jìn)制,也就是151,在 10 號 bucket 中尋找** tophash 值(HOB hash)為 151* 的 槽位**,即為key所在位置,找到了 2 號槽位,這樣整個(gè)查找過程就結(jié)束了。
如果在 bucket 中沒找到,并且 overflow 不為空,還要繼續(xù)去 overflow bucket 中尋找,直到找到或是所有的 key 槽位都找遍了,包括所有的 overflow bucket。
通過上面找到了對應(yīng)的槽位,這里我們再詳細(xì)分析下key/value值是如何獲取的:
bucket 里 key 的起始地址就是 unsafe.Pointer(b)+dataOffset。第 i 個(gè) key 的地址就要在此基礎(chǔ)上跨過 i 個(gè) key 的大?。欢覀冇种?,value 的地址是在所有 key 之后,因此第 i 個(gè) value 的地址還需要加上所有 key 的偏移。
通過匯編語言可以看到,向 map 中插入或者修改 key,最終調(diào)用的是 mapassign 函數(shù)。
實(shí)際上插入或修改 key 的語法是一樣的,只不過前者操作的 key 在 map 中不存在,而后者操作的 key 存在 map 中。
mapassign 有一個(gè)系列的函數(shù),根據(jù) key 類型的不同,編譯器會(huì)將其優(yōu)化為相應(yīng)的“快速函數(shù)”。
我們只用研究最一般的賦值函數(shù) mapassign 。
map的賦值會(huì)附帶著map的擴(kuò)容和遷移,map的擴(kuò)容只是將底層數(shù)組擴(kuò)大了一倍,并沒有進(jìn)行數(shù)據(jù)的轉(zhuǎn)移,數(shù)據(jù)的轉(zhuǎn)移是在擴(kuò)容后逐步進(jìn)行的,在遷移的過程中每進(jìn)行一次賦值(access或者delete)會(huì)至少做一次遷移工作。
1.判斷map是否為nil
每一次進(jìn)行賦值/刪除操作時(shí),只要oldbuckets != nil 則認(rèn)為正在擴(kuò)容,會(huì)做一次遷移工作,下面會(huì)詳細(xì)說下遷移過程
根據(jù)上面查找過程,查找key所在位置,如果找到則更新,沒找到則找空位插入即可
經(jīng)過前面迭代尋找動(dòng)作,若沒有找到可插入的位置,意味著需要擴(kuò)容進(jìn)行插入,下面會(huì)詳細(xì)說下擴(kuò)容過程
通過匯編語言可以看到,向 map 中刪除 key,最終調(diào)用的是 mapdelete 函數(shù)
刪除的邏輯相對比較簡單,大多函數(shù)在賦值操作中已經(jīng)用到過,核心還是找到 key 的具體位置。尋找過程都是類似的,在 bucket 中挨個(gè) cell 尋找。找到對應(yīng)位置后,對 key 或者 value 進(jìn)行“清零”操作,將 count 值減 1,將對應(yīng)位置的 tophash 值置成 Empty
再來說觸發(fā) map 擴(kuò)容的時(shí)機(jī):在向 map 插入新 key 的時(shí)候,會(huì)進(jìn)行條件檢測,符合下面這 2 個(gè)條件,就會(huì)觸發(fā)擴(kuò)容:
1、裝載因子超過閾值
源碼里定義的閾值是 6.5 (loadFactorNum/loadFactorDen),是經(jīng)過測試后取出的一個(gè)比較合理的因子
我們知道,每個(gè) bucket 有 8 個(gè)空位,在沒有溢出,且所有的桶都裝滿了的情況下,裝載因子算出來的結(jié)果是 8。因此當(dāng)裝載因子超過 6.5 時(shí),表明很多 bucket 都快要裝滿了,查找效率和插入效率都變低了。在這個(gè)時(shí)候進(jìn)行擴(kuò)容是有必要的。
對于條件 1,元素太多,而 bucket 數(shù)量太少,很簡單:將 B 加 1,bucket 最大數(shù)量( 2^B )直接變成原來 bucket 數(shù)量的 2 倍。于是,就有新老 bucket 了。注意,這時(shí)候元素都在老 bucket 里,還沒遷移到新的 bucket 來。新 bucket 只是最大數(shù)量變?yōu)樵瓉碜畲髷?shù)量的 2 倍( 2^B * 2 ) 。
2、overflow 的 bucket 數(shù)量過多
在裝載因子比較小的情況下,這時(shí)候 map 的查找和插入效率也很低,而第 1 點(diǎn)識(shí)別不出來這種情況。表面現(xiàn)象就是計(jì)算裝載因子的分子比較小,即 map 里元素總數(shù)少,但是 bucket 數(shù)量多(真實(shí)分配的 bucket 數(shù)量多,包括大量的 overflow bucket)
不難想像造成這種情況的原因:不停地插入、刪除元素。先插入很多元素,導(dǎo)致創(chuàng)建了很多 bucket,但是裝載因子達(dá)不到第 1 點(diǎn)的臨界值,未觸發(fā)擴(kuò)容來緩解這種情況。之后,刪除元素降低元素總數(shù)量,再插入很多元素,導(dǎo)致創(chuàng)建很多的 overflow bucket,但就是不會(huì)觸發(fā)第 1 點(diǎn)的規(guī)定,你能拿我怎么辦?overflow bucket 數(shù)量太多,導(dǎo)致 key 會(huì)很分散,查找插入效率低得嚇人,因此出臺(tái)第 2 點(diǎn)規(guī)定。這就像是一座空城,房子很多,但是住戶很少,都分散了,找起人來很困難
對于條件 2,其實(shí)元素沒那么多,但是 overflow bucket 數(shù)特別多,說明很多 bucket 都沒裝滿。解決辦法就是開辟一個(gè)新 bucket 空間,將老 bucket 中的元素移動(dòng)到新 bucket,使得同一個(gè) bucket 中的 key 排列地更緊密。這樣,原來,在 overflow bucket 中的 key 可以移動(dòng)到 bucket 中來。結(jié)果是節(jié)省空間,提高 bucket 利用率,map 的查找和插入效率自然就會(huì)提升。
由于 map 擴(kuò)容需要將原有的 key/value 重新搬遷到新的內(nèi)存地址,如果有大量的 key/value 需要搬遷,會(huì)非常影響性能。因此 Go map 的擴(kuò)容采取了一種稱為“漸進(jìn)式”的方式,原有的 key 并不會(huì)一次性搬遷完畢,每次最多只會(huì)搬遷 2 個(gè) bucket。
上面說的 hashGrow() 函數(shù)實(shí)際上并沒有真正地“搬遷”,它只是分配好了新的 buckets,并將老的 buckets 掛到了 oldbuckets 字段上。真正搬遷 buckets 的動(dòng)作在 growWork() 函數(shù)中,而調(diào)用 growWork() 函數(shù)的動(dòng)作是在 mapassign 和 mapdelete 函數(shù)中。也就是插入或修改、刪除 key 的時(shí)候,都會(huì)嘗試進(jìn)行搬遷 buckets 的工作。先檢查 oldbuckets 是否搬遷完畢,具體來說就是檢查 oldbuckets 是否為 nil。
如果未遷移完畢,賦值/刪除的時(shí)候,擴(kuò)容完畢后(預(yù)分配內(nèi)存),不會(huì)馬上就進(jìn)行遷移。而是采取 增量擴(kuò)容 的方式,當(dāng)有訪問到具體 bukcet 時(shí),才會(huì)逐漸的進(jìn)行遷移(將 oldbucket 遷移到 bucket)
nevacuate 標(biāo)識(shí)的是當(dāng)前的進(jìn)度,如果都搬遷完,應(yīng)該和2^B的長度是一樣的
在evacuate 方法實(shí)現(xiàn)是把這個(gè)位置對應(yīng)的bucket,以及其沖突鏈上的數(shù)據(jù)都轉(zhuǎn)移到新的buckets上。
轉(zhuǎn)移的判斷直接通過tophash 就可以,判斷tophash中第一個(gè)hash值即可
遍歷的過程,就是按順序遍歷 bucket,同時(shí)按順序遍歷 bucket 中的 key。
map遍歷是無序的,如果想實(shí)現(xiàn)有序遍歷,可以先對key進(jìn)行排序
為什么遍歷 map 是無序的?
如果發(fā)生過遷移,key 的位置發(fā)生了重大的變化,有些 key 飛上高枝,有些 key 則原地不動(dòng)。這樣,遍歷 map 的結(jié)果就不可能按原來的順序了。
如果就一個(gè)寫死的 map,不會(huì)向 map 進(jìn)行插入刪除的操作,按理說每次遍歷這樣的 map 都會(huì)返回一個(gè)固定順序的 key/value 序列吧。但是 Go 杜絕了這種做法,因?yàn)檫@樣會(huì)給新手程序員帶來誤解,以為這是一定會(huì)發(fā)生的事情,在某些情況下,可能會(huì)釀成大錯(cuò)。
Go 做得更絕,當(dāng)我們在遍歷 map 時(shí),并不是固定地從 0 號 bucket 開始遍歷,每次都是從一個(gè)**隨機(jī)值序號的 bucket 開始遍歷,并且是從這個(gè) bucket 的一個(gè) 隨機(jī)序號的 cell **開始遍歷。這樣,即使你是一個(gè)寫死的 map,僅僅只是遍歷它,也不太可能會(huì)返回一個(gè)固定序列的 key/value 對了。
對于某些類型的變量,如指針、切片、map、接口、通道、函數(shù)等,如果從未為它賦過值,則它將具有默認(rèn)值nil。這句代碼的意思就是,如果s0未初始化過,就打印true,否則打印false。
Get轉(zhuǎn)到定義是如下代碼,
func (c *Client) Get(url string) (resp *Response, err error) {
req, err := NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
return c.Do(req)
}
看上去已經(jīng)有足夠多的動(dòng)作了,并不是你說的只是一個(gè)接口啊
隊(duì)列的概念在 順序隊(duì)列 中,而使用循環(huán)隊(duì)列的目的主要是規(guī)避假溢出造成的空間浪費(fèi),在使用循環(huán)隊(duì)列處理假溢出時(shí),主要有三種解決方案
本文提供后兩種解決方案。
順序隊(duì)和循環(huán)隊(duì)列是一種特殊的線性表,與順序棧類似,都是使用一組地址連續(xù)的存儲(chǔ)單元依次存放自隊(duì)頭到隊(duì)尾的數(shù)據(jù)元素,同時(shí)附設(shè)隊(duì)頭(front)和隊(duì)尾(rear)兩個(gè)指針,但我們要明白一點(diǎn),這個(gè)指針并不是指針變量,而是用來表示數(shù)組當(dāng)中元素下標(biāo)的位置。
本文使用切片來完成的循環(huán)隊(duì)列,由于一開始使用三個(gè)參數(shù)的make關(guān)鍵字創(chuàng)建切片,在輸出的結(jié)果中不包含nil值(看起來很舒服),而且在驗(yàn)證的過程中發(fā)現(xiàn)使用append()函數(shù)時(shí)切片內(nèi)置的cap會(huì)發(fā)生變化,在消除了種種障礙后得到了一個(gè)四不像的循環(huán)隊(duì)列,即設(shè)置的指針是順序隊(duì)列的指針,但實(shí)際上進(jìn)行的操作是順序隊(duì)列的操作。最后是對make()函數(shù)和append()函數(shù)的一些使用體驗(yàn)和小結(jié),隊(duì)列的應(yīng)用放在鏈隊(duì)好了。
官方描述(片段)
即切片是一個(gè)抽象層,底層是對數(shù)組的引用。
當(dāng)我們使用
構(gòu)建出來的切片的每個(gè)位置的值都被賦為interface類型的初始值nil,但是nil值也是有大小的。
而使用
來進(jìn)行初始化時(shí),雖然生成的切片中不包含nil值,但是無法通過設(shè)置的指針變量來完成入隊(duì)和出隊(duì)的操作,只能使用append()函數(shù)來進(jìn)行操作
在go語言中,切片是一片連續(xù)的內(nèi)存空間加上長度與容量的標(biāo)識(shí),比數(shù)組更為常用。使用 append 關(guān)鍵字向切片中追加元素也是常見的切片操作
正是基于此,在使用go語言完成循環(huán)隊(duì)列時(shí),首先想到的就是使用make(type, len, cap)關(guān)鍵字方式完成切片初始化,然后使用append()函數(shù)來操作該切片,但這一方式出現(xiàn)了很多問題。在使用append()函數(shù)時(shí),切片的cap可能會(huì)發(fā)生變化,用不好就會(huì)發(fā)生擴(kuò)容或收縮。最終造成的結(jié)果是一個(gè)四不像的結(jié)果,入隊(duì)和出隊(duì)操作變得與指針變量無關(guān),失去了作為循環(huán)隊(duì)列的意義,用在順序隊(duì)列還算合適。
參考博客:
Go語言中的Nil
Golang之nil
Go 語言設(shè)計(jì)與實(shí)現(xiàn)