以網(wǎng)站域名做郵箱怎樣做企業(yè)宣傳推廣
文章目錄
- Golang面試題總結(jié)
- 一、基礎(chǔ)知識(shí)
- 1、defer相關(guān)
- 2、rune 類型
- 3、context包
- 4、Go 競(jìng)態(tài)、內(nèi)存逃逸分析
- 5、Goroutine 和線程的區(qū)別
- 6、Go 里面并發(fā)安全的數(shù)據(jù)類型
- 7、Go 中常用的并發(fā)模型
- 8、Go 中安全讀寫共享變量方式
- 9、Go 面向?qū)ο笫侨绾螌?shí)現(xiàn)的
- 10、make 和 new 的區(qū)別
- 11、Go 關(guān)閉(退出) goroutine 的方式
- 12、Gorm更新空值問(wèn)題
- 13、Go語(yǔ)言優(yōu)勢(shì)和缺點(diǎn)
- 14、Go 為什么要使用協(xié)程?
- 15、內(nèi)存對(duì)齊
- 16、反射
- 17、go中一個(gè)地址占多少位?
- 18、go 包循環(huán)引用?怎么避免?
- 19、go 閉包調(diào)用?怎么避免?
- 20、結(jié)構(gòu)體比較
- 21、go 語(yǔ)言中的可比較類型和不可比較類型
- 22、interface 接口
- 23、空結(jié)構(gòu)體 struct 應(yīng)用場(chǎng)景
- 24、go 內(nèi)存泄漏
- 25、協(xié)程泄露
- 26、值傳遞和地址傳遞(引用傳遞)
- 27、go 語(yǔ)言中棧的空間有多大?
- 28、并發(fā)情況下的數(shù)據(jù)處理,避免并發(fā)情況下數(shù)據(jù)競(jìng)爭(zhēng)問(wèn)題?
- 二、channel 通道
- 1、底層數(shù)據(jù)結(jié)構(gòu)
- 2、channel為什么能做到線程安全?
- 3、無(wú)緩沖的 channel 和 有緩沖的 channel 的區(qū)別?
- 4、channel 死鎖的場(chǎng)景
- 5、操作 channel 的情況總結(jié)
- 三、map 哈希表
- 1、map 的底層數(shù)據(jù)結(jié)構(gòu)是什么?
- 2、map的擴(kuò)容
- 3、從未初始化的 map 讀數(shù)據(jù)會(huì)發(fā)生什么?
- 4、map 中的 key 為什么是無(wú)序的?怎么實(shí)現(xiàn)有序?
- 5、map并發(fā)訪問(wèn)安全嗎?怎么解決?可以邊遍歷邊刪除嗎?
- 6、map元素可以取地址嗎?
- 7、map 中刪除一個(gè) key,它的內(nèi)存會(huì)釋放么?
- 8、什么樣的類型可以做 map 的鍵 key?
- 9、如何比較兩個(gè) map 相等?
- 10、map怎么解決哈希沖突?
- 11、map 使用中注意的點(diǎn)?
- 12、map 創(chuàng)建、賦值、刪除、查詢的過(guò)程?
- 四、slice 切片
- 1、數(shù)組和切片的區(qū)別
- 2、slice 底層數(shù)據(jù)結(jié)構(gòu)
- 3、slice 的擴(kuò)容
- 4、slice 的拷貝
- 5、append 函數(shù)
- 6、切片作為函數(shù)參數(shù)?
- 7、切片 slice 使用時(shí)注意的點(diǎn)?
- 8、slice 內(nèi)存泄露情況
- 9、slice 并發(fā)不安全
- 10、從未初始化的 slice讀數(shù)據(jù)會(huì)發(fā)生什么?
Golang面試題總結(jié)
一、基礎(chǔ)知識(shí)
1、defer相關(guān)
??defer是Go語(yǔ)言中的一個(gè)關(guān)鍵字,延遲調(diào)用。
-
作用
??釋放資源、釋放鎖、關(guān)閉文件、關(guān)閉鏈接、捕獲panic等收尾工作。 -
執(zhí)行順序
??多個(gè) defer 調(diào)用順序是 LIFO(后入先出),defer后的操作可以理解為壓入棧中,函數(shù)參數(shù)會(huì)被拷?下來(lái)。
??defer聲明時(shí),對(duì)應(yīng)的參數(shù)會(huì)實(shí)時(shí)解析。
??defer、return、返回值三者的執(zhí)行邏輯:return最先執(zhí)行,return負(fù)責(zé)將結(jié)果寫入返回值中;接著defer開(kāi)始執(zhí)行一些收尾工作;最后函數(shù)攜帶當(dāng)前返回值(可能和最初的返回值不相同)退出。
??defer與panic的執(zhí)行邏輯:在panic語(yǔ)句后面的defer語(yǔ)句不被執(zhí)行,在panic語(yǔ)句前的defer語(yǔ)句會(huì)被執(zhí)行(早于panic),panic觸發(fā)defer出棧??梢栽赿efer中使用recover捕獲異常,panic 后依然有效。panic僅有最后一個(gè)可以被revover捕獲。 -
defer與recover
??recover(異常捕獲)可以讓程序在引發(fā)panic的時(shí)候不會(huì)崩潰退出。在引發(fā)panic的時(shí)候,panic會(huì)停掉當(dāng)前正在執(zhí)?的程序,但是,在這之前,它會(huì)有序的執(zhí)?完當(dāng)前goroutine的defer列表中的語(yǔ)句。
??我們通常在defer??掛?個(gè)recover,防?程序直接掛掉,類似于try…catch,但絕對(duì)不能像try…catch這樣使?,因?yàn)閜anic的作?不是為了捕獲異常。recover函數(shù)只在defer的上下?中才有效,如果直接調(diào)?recover,會(huì)返回nil。
??recover不能跨協(xié)程捕獲panic信息。recover只能恢復(fù)同一個(gè)協(xié)程中的panic,所以必須與可能發(fā)生panic的協(xié)程在同一個(gè)協(xié)程中才生效。panic在子協(xié)程中,而recover在主協(xié)程中,最終會(huì)導(dǎo)致所有的協(xié)程全部掛掉,程序會(huì)整體退出。 -
defer可以修改函數(shù)最終返回值
??修改時(shí)機(jī):有名返回值或者函數(shù)返回指針。
??無(wú)名返回(返回值沒(méi)有指定命名),執(zhí)行Return語(yǔ)句后,Go會(huì)創(chuàng)建一個(gè)臨時(shí)變量保存返回值,defer修改的是臨時(shí)變量,沒(méi)有修改返回值。
??有名返回(指定返回值命名func test() (t int)),執(zhí)行 return 語(yǔ)句時(shí),并不會(huì)再創(chuàng)建臨時(shí)變量保存,defer修改的是返回值。
??函數(shù)返回值為指針,指向變量所在的地址,defer修改變量,指針指向的地址不變,地址對(duì)應(yīng)的內(nèi)容發(fā)生了改變,返回值改變。
??特殊例子:defer沒(méi)有修改有名返回值,因?yàn)?r 作為參數(shù),傳入defer 內(nèi)部時(shí)會(huì)發(fā)生值拷貝,地址會(huì)變,defer修改的是新地址的變量,不是原來(lái)的返回值。
func f() (r int) {defer func(r int) {r = r + 5}(r)return r
}
//0
為什么defer要按照定義的順序逆序執(zhí)行?
??后?定義的函數(shù)可能會(huì)依賴前?的資源,所以要先執(zhí)?。如果前?先執(zhí)?,釋放掉這個(gè)依賴,那后?的函數(shù)就找不到它的依賴了。
3、defer函數(shù)定義時(shí),對(duì)外部變量的引??式有兩種
分別是函數(shù)參數(shù)以及作為閉包引?。
在作為函數(shù)參數(shù)的時(shí)候,在defer定義時(shí)就把值傳遞給defer,并被緩存起來(lái)。
如果是作為閉包引?,則會(huì)在defer真正調(diào)?的時(shí)候,根據(jù)整個(gè)上下?去確定當(dāng)前的值。
4、defer后?的語(yǔ)句在執(zhí)?的時(shí)候,函數(shù)調(diào)?的參數(shù)會(huì)被保存起來(lái),也就是復(fù)制。在真正執(zhí)?的時(shí)候,實(shí)際上?到的是復(fù)制的變量,也就是說(shuō),如果這個(gè)變量是?個(gè)"值類型",那他就和定義的時(shí)候是?致的,如果是?個(gè)"引?",那么就可能和定義的時(shí)候的值不?致。
2、rune 類型
??Go語(yǔ)言的字符有以下兩種:
- byte 等同于uint8,常用來(lái)處理 ASCII 碼;
- rune 等同于int32,常用來(lái)處理 unicode 或 utf-8 字符。當(dāng)需要處理中文、日文或者其他復(fù)合字符時(shí),使用 rune 類型。
??在 Go 語(yǔ)言中,字符串默認(rèn)使用 UTF-8 編碼,UTF8 的好處在于,如果基本是英文,每個(gè)字符占 1 byte,和 ASCII 編碼是一樣的,非常節(jié)省空間,如果是中文,一般占3字節(jié)。中文字符在unicode下占2個(gè)字節(jié),在utf-8編碼下占3個(gè)字節(jié)。
在Go中,字符串是以UTF-8編碼格式進(jìn)行存儲(chǔ)的。在UTF-8編碼中,一個(gè)漢字通常占用24位(3字節(jié))。英文字符(包括英文標(biāo)點(diǎn))通常占用8位(1字節(jié))1個(gè)字節(jié)。
??字符串的底層表示是 byte (8 bit) 序列,而非 rune (32 bit) 序列。包含中文的字符串,正確的處理方式是將 string 轉(zhuǎn)為 rune 數(shù)組,轉(zhuǎn)換成 []rune 類型后,字符串中的每個(gè)字符,無(wú)論占多少個(gè)字節(jié)都用 int32 來(lái)表示,因而可以正確處理中文。
func main() {str := "我愛(ài)GO"fmt.Println(reflect.TypeOf(str[0]).Kind()) //uint8fmt.Println(len(str)) //8runeStr := []rune(str)runeStr[0] = '你'fmt.Println(reflect.TypeOf(runeStr[0]).Kind()) //int32fmt.Println(string(runeStr)) //你愛(ài)GOfmt.Println(len(runeStr)) //4
}
// reflect.TypeOf().Kind() 可以知道某個(gè)變量的類型
3、context包
??context是Golang常用的并發(fā)控制技術(shù),context的作用就是在不同的goroutine之間同步請(qǐng)求特定的數(shù)據(jù)、取消信號(hào)以及處理請(qǐng)求的截止日期(設(shè)置超時(shí)時(shí)間)。目前我們常用的一些庫(kù)都是支持context的,例如gin、database/sql等庫(kù)都是支持context的,這樣更方便我們做并發(fā)控制了,只要在服務(wù)器入口創(chuàng)建一個(gè)context上下文,不斷透?jìng)飨氯ゼ纯伞?/p>
??context可以用來(lái)在goroutine之間傳遞上下文信息,相同的context可以傳遞給運(yùn)行在不同goroutine中的函數(shù),上下文對(duì)于多個(gè)goroutine同時(shí)使用是安全的,它與WaitGroup最大的不同點(diǎn)是context對(duì)于派生goroutine有更強(qiáng)的控制力,它可以控制多級(jí)的goroutine。
??context包定義了上下文類型,可以使用context.Background()、context.TODO()創(chuàng)建一個(gè)上下文,也可以使用WithDeadline()、WithTimeout()、WithCancel() 或 WithValue() 創(chuàng)建。
??Go 的 Context 的數(shù)據(jù)結(jié)構(gòu)包含 Deadline(),Done(),Err(),Value()方法:
- Deadline() 方法返回一個(gè)time.Time和一個(gè)bool值。time.Time表示此上下文被取消的時(shí)間,也就是完成工作的截至日期。布爾值表示是否設(shè)置截止日期,當(dāng)沒(méi)有設(shè)置截止日期時(shí),bool值返回ok==false,此時(shí)截止日期為一個(gè)初始值的time.Time值。對(duì)Deadline的連續(xù)調(diào)用返回相同的結(jié)果。
- Done() 方法返回一個(gè)channel。 這個(gè) Channel 會(huì)在當(dāng)前工作完成或者context 被取消之后關(guān)閉,告訴給 context 相關(guān)的函數(shù)要停止當(dāng)前工作然后返回了。對(duì)Done的連續(xù)調(diào)用返回相同的值。需要在select-case語(yǔ)句中使用,如”case <-context.Done():”。當(dāng)context關(guān)閉后,Done()返回一個(gè)被關(guān)閉的管道,關(guān)閉的管道仍然是可讀的,據(jù)此goroutine可以收到關(guān)閉請(qǐng)求;當(dāng)context還未關(guān)閉時(shí),Done()返回nil。
- Err() 方法返回一個(gè)error。表示 context 被關(guān)閉的原因,關(guān)閉原因由context實(shí)現(xiàn)控制,不需要用戶設(shè)置。當(dāng)context關(guān)閉后,Err()返回context的關(guān)閉原因;當(dāng)context還未關(guān)閉時(shí),Err()返回nil。Context 被取消,返回 “context canceled” 錯(cuò)誤;超時(shí),返回“context deadline exceeded”錯(cuò)誤。
- Value() 方法,參數(shù)為key,返回 Context 中 key 對(duì)應(yīng)的值,如果沒(méi)有值與鍵相關(guān)聯(lián),返回nil。對(duì)于同一個(gè) Context 來(lái)說(shuō),多次調(diào)用Value() 并傳入相同的Key,會(huì)返回相同的結(jié)果,這個(gè)功能可以用來(lái)傳遞特定的數(shù)據(jù)。
4、Go 競(jìng)態(tài)、內(nèi)存逃逸分析
??競(jìng)態(tài):資源競(jìng)爭(zhēng),就是在程序中,同一塊內(nèi)存同時(shí)被多個(gè) goroutine 訪問(wèn)。我們使用 go build、go run、go test 命令時(shí),可以添加 -race 標(biāo)識(shí)檢查代碼中是否存在資源競(jìng)爭(zhēng)。
??解決方案:給資源進(jìn)行加鎖,讓其在同一時(shí)刻只能被一個(gè)協(xié)程來(lái)操作。
??sync.Mutex
??sync.RWMutex
??內(nèi)存逃逸分析:是go的編譯器在編譯期間,對(duì)代碼進(jìn)行分析,根據(jù)變量的類型和作用域,確定變量是分配在堆上還是棧上,如果變量需要分配在堆上,則稱作內(nèi)存逃逸了。簡(jiǎn)單來(lái)說(shuō),本該分配到棧上的變量,跑到了堆上,這就導(dǎo)致了內(nèi)存逃逸。go的編譯器提供了逃逸分析的工具,只需要在編譯的時(shí)候加上 -gcflags=-m 就可以看到逃逸分析的結(jié)果了。
??為什么需要逃逸分析?
??因?yàn)間o語(yǔ)言是自動(dòng)內(nèi)存管理的,也就是有GC的。開(kāi)發(fā)者在寫代碼的時(shí)候不需要關(guān)心考慮內(nèi)存釋放的問(wèn)題,這樣編譯器和go運(yùn)行時(shí)(runtime)就需要準(zhǔn)確分配和管理內(nèi)存,所以編譯器在編譯期間要確定變量是放在堆空間和??臻g。
??棧是高地址到低地址,棧上的變量,函數(shù)結(jié)束后變量會(huì)跟著回收掉,不會(huì)有額外性能的開(kāi)銷。變量從棧逃逸到堆上,如果要回收掉,需要進(jìn)行 gc,那么 gc 一定會(huì)帶來(lái)額外的性能開(kāi)銷。編程語(yǔ)言不斷優(yōu)化 gc 算法,主要目的都是為了減少 gc 帶來(lái)的額外性能開(kāi)銷,變量一旦逃逸會(huì)導(dǎo)致性能開(kāi)銷變大。
??出現(xiàn)內(nèi)存逃逸的場(chǎng)景:
- 指針逃逸
??返回局部變量的指針、向 channel 發(fā)送指針或帶有指針的數(shù)據(jù)、在 slice 或 map 中存儲(chǔ)指針或帶有指針的值。 - 動(dòng)態(tài)類型逃逸
??當(dāng)函數(shù)傳遞的變量類型是 interface{} 類型的時(shí)候,因?yàn)榫幾g器無(wú)法推斷運(yùn)行時(shí)變量的實(shí)際類型,所以也會(huì)發(fā)生逃逸。 - ??臻g不足逃逸
??切片(擴(kuò)容后)長(zhǎng)度太大,因?yàn)闂5目臻g是有限的,所以在分配大塊內(nèi)存時(shí),會(huì)考慮??臻g內(nèi)否存下,如果??臻g存不下,會(huì)分配到堆上。 - 閉包引用對(duì)象逃逸
??在閉包中引用包外的值。
5、Goroutine 和線程的區(qū)別
-
進(jìn)程:進(jìn)程是系統(tǒng)進(jìn)行資源分配和調(diào)度的一個(gè)獨(dú)立單位,分配完整獨(dú)立的地址空間,擁有自己獨(dú)立的堆和棧,既不共享堆,亦不共享?xiàng)?#xff0c;進(jìn)程的切換只發(fā)生在內(nèi)核態(tài),由操作系統(tǒng)調(diào)度,進(jìn)程間的切換開(kāi)銷(棧、寄存器、虛擬內(nèi)存、文件句柄等)比較大,但相對(duì)比較穩(wěn)定安全。不同進(jìn)程通過(guò)進(jìn)程間通信來(lái)通信。
-
線程:是CPU調(diào)度和分派的基本單位,和其它本進(jìn)程的線程共享地址空間,擁有自己獨(dú)立的棧和共享的堆,共享堆,不共享?xiàng)?#xff0c;線程的切換一般也由操作系統(tǒng)調(diào)度(標(biāo)準(zhǔn)線程是的)。 線程間通信主要通過(guò)共享內(nèi)存,上下文切換很快,資源開(kāi)銷較少,但相比進(jìn)程不夠穩(wěn)定容易丟失數(shù)據(jù)。
-
協(xié)程:協(xié)程是一種用戶態(tài)的輕量級(jí)線程,協(xié)程的調(diào)度完全由用戶控制。共享堆,不共享?xiàng)?。協(xié)程擁有自己的寄存器上下文和棧。協(xié)程調(diào)度切換時(shí),將寄存器上下文和棧保存到其他地方,在切回來(lái)的時(shí)候,恢復(fù)先前保存的寄存器上下文和棧,直接操作棧則基本沒(méi)有內(nèi)核切換的開(kāi)銷,可以不加鎖的訪問(wèn)全局變量,所以上下文的切換非???。
??進(jìn)程和線程的切換主要依賴于時(shí)間片的控制,而協(xié)程的切換則主要依賴于自身。
??goroutine是非常輕量級(jí)的,它就是一段代碼,一個(gè)函數(shù)入口,以及在堆上為其分配的一個(gè)堆棧(在64位機(jī)器上,初始化大小為2KB,最大為1GB,會(huì)隨著程序的執(zhí)行自動(dòng)增長(zhǎng)刪除),所以它非常廉價(jià),我們可以很輕松的創(chuàng)建上萬(wàn)個(gè)goroutine。
??在 go 語(yǔ)言中,每個(gè) goroutine 默認(rèn)使用比較小的??臻g(通常只有幾 kb),用于保存其執(zhí)行狀態(tài)、臨時(shí)變量等數(shù)據(jù)。如果需要更大的??臻g,則會(huì)動(dòng)態(tài)地進(jìn)行擴(kuò)容,最終實(shí)現(xiàn)在堆上分配內(nèi)存,并將原來(lái)的棧數(shù)據(jù)復(fù)制到新的堆內(nèi)存位置中。
??因此,可以說(shuō) goroutine 在創(chuàng)建時(shí)先分配在棧上,當(dāng)彈出棧無(wú)法滿足內(nèi)存需求時(shí),將重新分配在堆上。但從整體上看,go 運(yùn)行時(shí)負(fù)責(zé)管理所有 goroutine 的內(nèi)存分配,具體實(shí)現(xiàn)方式對(duì)于開(kāi)發(fā)者來(lái)說(shuō)可能并不透明或關(guān)鍵。
6、Go 里面并發(fā)安全的數(shù)據(jù)類型
(1)由一條機(jī)器指令完成賦值的類型并發(fā)賦值是安全的,這些類型有:字節(jié)型,布爾型、整型、浮點(diǎn)型、字符型、指針、函數(shù)。
(2)數(shù)組由一個(gè)或多個(gè)元素組成,大部分情況并發(fā)不安全。注意:當(dāng)位寬不大于 64 位且是 2 的整數(shù)次冪(8,16,32,64),那么其并發(fā)賦值是安全的。
(3)struct 或底層是 struct 的類型并發(fā)賦值大部分情況并發(fā)不安全,這些類型有:復(fù)數(shù)、字符串、 數(shù)組、切片 slice、字典 map、接口 interface。
??注意:當(dāng) struct 賦值時(shí)退化為單個(gè)字段由一個(gè)機(jī)器指令完成賦值時(shí),并發(fā)賦值又是安全的。這種情況有:
(a)實(shí)部或虛部相同的復(fù)數(shù)的并發(fā)賦值;
(b)等長(zhǎng)字符串的并發(fā)賦值;
(c)同長(zhǎng)度同容量切片的并發(fā)賦值;
(d)同一種具體類型不同值并發(fā)賦給接口。
7、Go 中常用的并發(fā)模型
-
通過(guò)channel通知實(shí)現(xiàn)并發(fā)控制
-
通過(guò)sync包中的WaitGroup實(shí)現(xiàn)并發(fā)控制
??Goroutine是異步執(zhí)行的,有的時(shí)候?yàn)榱朔乐乖诮Y(jié)束mian函數(shù)的時(shí)候結(jié)束掉Goroutine,所以需要同步等待,這個(gè)時(shí)候就需要用 WaitGroup了,在 sync 包中,提供了 WaitGroup ,它會(huì)等待它收集的所有 goroutine 任務(wù)全部完成。在WaitGroup里主要有三個(gè)方法:Add()可以添加或減少 goroutine的數(shù)量,Done()相當(dāng)于Add(-1),Wait()執(zhí)行后會(huì)堵塞主線程,直到WaitGroup 里的值減至0。
??在主 goroutine 中 Add(delta int) 索要等待goroutine 的數(shù)量,在每一個(gè) goroutine 完成后 Done() 表示這一個(gè)goroutine 已經(jīng)完成,當(dāng)所有的 goroutine 都完成后,在主 goroutine 中 WaitGroup 返回返回。 -
在Go 1.7 以后引進(jìn)的強(qiáng)大的Context上下文,實(shí)現(xiàn)并發(fā)控制
8、Go 中安全讀寫共享變量方式
go中除了加Mutex鎖以外還有哪些方式安全讀寫共享變量?
??go 中 Goroutine 可以通過(guò) Channel 進(jìn)行安全讀寫共享變量,而且官網(wǎng)建議使用這種方式,此方式的并發(fā)是由官方進(jìn)行保證的。
map
在并發(fā)情況下,只讀是線程安全的,同時(shí)讀寫是線程不安全的。
1、在寫操作的時(shí)候增加鎖
2、sync.Map包
數(shù)組
指定索引進(jìn)行讀寫時(shí),數(shù)組是支持并發(fā)讀寫索引區(qū)的數(shù)據(jù)的,但是索引區(qū)的數(shù)據(jù)在并發(fā)時(shí)會(huì)被覆蓋的;
1、加鎖
2、控制并發(fā)順序
切片
指定索引進(jìn)行讀寫:是支持并發(fā)讀寫索引區(qū)的數(shù)據(jù)的,但是索引區(qū)的數(shù)據(jù)在并發(fā)時(shí)可能會(huì)被覆蓋的;
發(fā)生切片動(dòng)態(tài)擴(kuò)容:并發(fā)場(chǎng)景下擴(kuò)容可能會(huì)被覆蓋。
1、加互斥鎖
2、使用channel串行化操作
3、使用sync.map代替切片
9、Go 面向?qū)ο笫侨绾螌?shí)現(xiàn)的
??Go實(shí)現(xiàn)面向?qū)ο蟮膬蓚€(gè)關(guān)鍵是struct和interface。
??封裝:對(duì)于同一個(gè)包,對(duì)象對(duì)包內(nèi)的文件可見(jiàn);對(duì)不同的包,需要將對(duì)象以大寫字母開(kāi)頭才是可見(jiàn)的。
??繼承:繼承是編譯時(shí)特征,go語(yǔ)言通過(guò)結(jié)構(gòu)體組合來(lái)實(shí)現(xiàn)繼承,在struct內(nèi)加入所需要繼承的類即可。Go支持多重繼承,就是在類型中嵌入所有必要的父類型。
type A struct{}
type B struct{A
}
??多態(tài):多態(tài)是運(yùn)行時(shí)特征,Go多態(tài)通過(guò)interface來(lái)實(shí)現(xiàn)。類型和接口是松耦合的,某個(gè)類型的實(shí)例可以賦給它所實(shí)現(xiàn)的任意接口類型的變量。
10、make 和 new 的區(qū)別
-
相同點(diǎn):
(1)都是給變量分配內(nèi)存;
(2)都是在堆上分配內(nèi)存。 -
不同點(diǎn):
(1)作用變量類型不同,new給string,int和數(shù)組分配內(nèi)存,make給切片,map,channel分配內(nèi)存;
(2)返回類型不一樣,new返回指向變量的指針,make返回變量本身;
(3)new 分配的空間被清零,make 分配空間后,會(huì)進(jìn)行初始化;
11、Go 關(guān)閉(退出) goroutine 的方式
- 向退出通道發(fā)送退出信號(hào)(退出一個(gè) goroutine)
- 關(guān)閉退出通道(退出多個(gè) goroutine)
- 使用 context.WithCancel() 方法,手動(dòng)調(diào)用 cancel() 方法退出 goroutine
- 使用 context.WithTimeout() 方法,手動(dòng)調(diào)用 cancel() 方法,在超時(shí)之前退出 goroutine
- 使用 context.WithDeadLine() 方法,在指定的時(shí)間退出 goroutine
12、Gorm更新空值問(wèn)題
問(wèn)題:在使用gorm將一個(gè)字段更新為空的時(shí)候,發(fā)現(xiàn)并不生效?
原因:通過(guò) struct 結(jié)構(gòu)體變量更新字段值,gorm 會(huì)忽略零值字段,如果更新后的值為0, nil, “”, false,就不會(huì)更新該字段,而是只更新非空的其他字段。
解決方案:
(1)使用 map 類型替代 struct 結(jié)構(gòu)體,更新傳值的時(shí)候通過(guò) map 來(lái)指定,key為字符串,value為 interface{} 類型,方便保存任意值。
(2)采用save的方式,先take獲取源數(shù)據(jù),然后在save進(jìn)行保存。
(3)修改gorm的源碼包,讓它支持自定義是否可以設(shè)置為空值。
13、Go語(yǔ)言優(yōu)勢(shì)和缺點(diǎn)
優(yōu)勢(shì):
1、并發(fā)編程:Go語(yǔ)言天生支持并發(fā),內(nèi)置了輕量級(jí)的協(xié)程(goroutine)和通信機(jī)制(channel),使得并發(fā)編程變得非常簡(jiǎn)單。這種并發(fā)模型是Go語(yǔ)言最大的特點(diǎn)之一,也是其在網(wǎng)絡(luò)編程、高并發(fā)處理等領(lǐng)域得以廣泛應(yīng)用的原因。
2、高效性能:Go語(yǔ)言使用靜態(tài)編譯,可以生成本地代碼,且具有快速的垃圾回收機(jī)制,使得其在性能上有很好的表現(xiàn)。
3、簡(jiǎn)單易學(xué):Go語(yǔ)言的語(yǔ)法簡(jiǎn)潔清晰,易于學(xué)習(xí)和理解,并且沒(méi)有像C++或Java那樣復(fù)雜的繼承、多態(tài)等概念。
4、跨平臺(tái)支持:Go語(yǔ)言提供了跨平臺(tái)的編譯工具,可以在不同操作系統(tǒng)和硬件上編譯出可執(zhí)行文件。
5、開(kāi)源社區(qū)支持:Go語(yǔ)言擁有一個(gè)龐大的開(kāi)源社區(qū),提供了大量的第三方庫(kù)和工具,可以方便地實(shí)現(xiàn)各種功能。
性能高、編譯快、開(kāi)發(fā)效率和運(yùn)行效率高:Go性能與 Java 或 C++相似,比C++開(kāi)發(fā)效率高,比php和python快,與Java比,更簡(jiǎn)明的類型系統(tǒng),go在語(yǔ)法簡(jiǎn)明和類型系統(tǒng)設(shè)計(jì)上優(yōu)于python。
豐富的標(biāo)準(zhǔn)庫(kù):Go目前已經(jīng)內(nèi)置了大量的庫(kù),特別是網(wǎng)絡(luò)庫(kù)非常強(qiáng)大。
內(nèi)置強(qiáng)大的工具:內(nèi)置了很多工具鏈,Go擁有強(qiáng)大的編譯檢查、嚴(yán)格的編碼規(guī)范和完整的軟件生命周期工具,具有很強(qiáng)的穩(wěn)定性,Go提供了軟件生命周期(開(kāi)發(fā)、測(cè)試、部署、維護(hù)等等)的各個(gè)環(huán)節(jié)的工具,如go tool、gofmt、go test。
缺點(diǎn):
1、生態(tài)系統(tǒng)相對(duì)不夠完善:雖然Go語(yǔ)言擁有龐大的開(kāi)源社區(qū),但相對(duì)其他成熟的編程語(yǔ)言如Python和Java,其生態(tài)系統(tǒng)還不夠完善,一些開(kāi)發(fā)工具和庫(kù)的支持還不夠全面。
2、語(yǔ)言特性相對(duì)較少:為了保持簡(jiǎn)潔,Go語(yǔ)言在一些高級(jí)特性上犧牲了部分靈活性。例如,Go語(yǔ)言沒(méi)有泛型、繼承、異常等概念,這些限制可能會(huì)影響一些特定領(lǐng)域的開(kāi)發(fā)。
3、不適合大型項(xiàng)目:雖然Go語(yǔ)言在小型或中型項(xiàng)目中表現(xiàn)優(yōu)異,但由于其缺乏一些高級(jí)特性和完善的生態(tài)系統(tǒng)支持,不適合用于大型復(fù)雜項(xiàng)目的開(kāi)發(fā)。
14、Go 為什么要使用協(xié)程?
??協(xié)程是一種用戶態(tài)的輕量級(jí)線程,協(xié)程的調(diào)度完全由用戶控制,由于協(xié)程運(yùn)行在用戶態(tài),能夠大大減少上下文切換帶來(lái)的開(kāi)銷,并且協(xié)程調(diào)度器把可運(yùn)行的協(xié)程逐個(gè)調(diào)度到線程中執(zhí)行,同時(shí)及時(shí)把阻塞的協(xié)程調(diào)度出線程,從而有效的避免了線程的頻繁切換,達(dá)到了使用少量的線程實(shí)現(xiàn)高并發(fā)的效果,但是對(duì)于一個(gè)線程來(lái)說(shuō)每一時(shí)刻只能運(yùn)行一個(gè)協(xié)程。
??高并發(fā)+高擴(kuò)展性+低成本:一個(gè)CPU支持上萬(wàn)的協(xié)程都不是問(wèn)題。所以很適合用于高并發(fā)處理。
15、內(nèi)存對(duì)齊
1、什么是內(nèi)存對(duì)齊?
??編譯器會(huì)按照特定的規(guī)則,把數(shù)據(jù)安排到合適的存儲(chǔ)地址上,并占用合適的地址長(zhǎng)度。每種類型的對(duì)齊值就是它的對(duì)齊邊界,內(nèi)存對(duì)齊要求數(shù)據(jù)存儲(chǔ)地址以及占用的字節(jié)數(shù)都要是它的對(duì)齊邊界的倍數(shù)。所以下述的int32要錯(cuò)開(kāi)兩個(gè)字節(jié),從4開(kāi)始存,卻不能緊接著從2開(kāi)始。
2、為什么要內(nèi)存對(duì)齊?
??內(nèi)存對(duì)齊是為了減少CPU訪問(wèn)內(nèi)存的次數(shù),加大 CPU 訪問(wèn)內(nèi)存的吞吐量,提高CPU讀取內(nèi)存數(shù)據(jù)的效率,可以讓CPU快速?gòu)膬?nèi)存中讀取到數(shù)據(jù),保證程序高效的運(yùn)行,避免資源浪費(fèi)。如果內(nèi)存不對(duì)齊,訪問(wèn)相同的數(shù)據(jù)需要多次的訪問(wèn)內(nèi)存。
??CPU 不會(huì)以一個(gè)字節(jié)一個(gè)字節(jié)的去讀取和寫入內(nèi)存,CPU 讀取內(nèi)存是一塊一塊讀取的,一塊內(nèi)存可以是2、4、8、16個(gè)字節(jié),塊大小稱為內(nèi)存訪問(wèn)粒度,內(nèi)存訪問(wèn)粒度跟機(jī)器字長(zhǎng)有關(guān),32位CPU訪問(wèn)粒度是4個(gè)字節(jié),64位CPU訪問(wèn)粒度是8個(gè)字節(jié)。
??平臺(tái)原因(移植原因):不是所有的硬件平臺(tái)都能訪問(wèn)任意地址上的任意數(shù)據(jù)的;某些硬件平臺(tái)只能在某些地址處取某些特定類型的數(shù)據(jù),否則拋出硬件異常。
??性能原因:數(shù)據(jù)結(jié)構(gòu)(尤其是棧)應(yīng)該盡可能地在自然邊界上對(duì)齊。原因在于,為了訪問(wèn)未對(duì)齊的內(nèi)存,處理器需要作兩次內(nèi)存訪問(wèn);而對(duì)齊的內(nèi)存訪問(wèn)僅需要一次訪問(wèn)。
3、內(nèi)存對(duì)齊規(guī)則
??起始的存儲(chǔ)地址 必須是 內(nèi)存對(duì)齊邊界 的倍數(shù)。
??整體占用字節(jié)數(shù) 必須是 內(nèi)存對(duì)齊邊界 的倍數(shù)。
16、反射
??Go語(yǔ)言的反射是指在運(yùn)行時(shí)動(dòng)態(tài)地獲取類型信息和操作對(duì)象的能力。這意味著程序可以檢查變量的類型、值,以及調(diào)用它們的方法。Go語(yǔ)言中的反射由reflect包提供支持,包括了一些常用的函數(shù)和數(shù)據(jù)類型,如TypeOf()、ValueOf()等。使用反射可以實(shí)現(xiàn)一些靈活的功能,如實(shí)現(xiàn)通用的序列化和反序列化,或者通過(guò)動(dòng)態(tài)調(diào)用方法來(lái)實(shí)現(xiàn)類似于插件的機(jī)制。但是需要注意的是,過(guò)度使用反射可能會(huì)影響代碼的可讀性和性能,因此應(yīng)該謹(jǐn)慎使用。
17、go中一個(gè)地址占多少位?
??在管理內(nèi)存地址的硬件/操作系統(tǒng)上,Go語(yǔ)言中的指針通常使用64位(8字節(jié))來(lái)表示。
18、go 包循環(huán)引用?怎么避免?
1、為什么會(huì)出現(xiàn)循環(huán)引用問(wèn)題?怎么發(fā)現(xiàn)?如何避免?
??程序結(jié)構(gòu)沒(méi)設(shè)計(jì)好,包沒(méi)有規(guī)劃好。
??有一個(gè)項(xiàng)目引用可視化的工具 godepgraph,可以查看包引用的關(guān)系,生成引用圖。
??項(xiàng)目框架構(gòu)建的時(shí)候,將各個(gè)模塊設(shè)計(jì)好,規(guī)劃好包。嘗試分層的設(shè)計(jì),高層依賴于低層,低層不依賴于高層,嚴(yán)格規(guī)范單向調(diào)用鏈,如控制層->業(yè)務(wù)層->數(shù)據(jù)層。
2、go 為什么不允許循環(huán)引用?
- 加快編譯速度;
- 規(guī)范框架設(shè)計(jì),使項(xiàng)目結(jié)構(gòu)更加清晰明了。
3、怎么解決?
- 對(duì)于軟相互依賴,利用分包的方法就能解決,有些函數(shù)導(dǎo)致的相互依賴只能通過(guò)分包解決;分包能細(xì)化包的功能;
軟相互依賴可以通過(guò)將方法拆分到另一個(gè)包的方式來(lái)解決;在拆分包的過(guò)程中,可能會(huì)將結(jié)構(gòu)體的方法轉(zhuǎn)化為普通的函數(shù);
- 對(duì)于硬相互依賴只能通過(guò)定義接口的方法解決;定義接口能提高包的獨(dú)立性,同時(shí)也提高了追蹤代碼調(diào)用關(guān)系的難度。
在 package b 中 定義 a interface ; 將 b 所有使用到結(jié)構(gòu)體 a 的變量和方法的地方全部轉(zhuǎn)化成 使用接口 a 的方法;在 a interface 中補(bǔ)充缺少的方法;
19、go 閉包調(diào)用?怎么避免?
1、什么是閉包調(diào)用?
??閉包是指有權(quán)訪問(wèn)另一個(gè)函數(shù)作用域中的變量的函數(shù),創(chuàng)建閉包的常見(jiàn)方式就是在一個(gè)函數(shù)內(nèi)部創(chuàng)建另一個(gè)函數(shù), 內(nèi)函數(shù)可以訪問(wèn)外函數(shù)的變量。
??注意:閉包里作用域返回的局部變量不會(huì)被立刻銷毀回收,可能會(huì)占用更多內(nèi)存,過(guò)度使用閉包會(huì)導(dǎo)致性能下降。
2、帶來(lái)的問(wèn)題?
??由于閉包會(huì)在其生命周期內(nèi)保留對(duì)環(huán)境變量的引用,因此可能會(huì)導(dǎo)致一些問(wèn)題,例如:
- 內(nèi)存泄漏:如果閉包持有對(duì)某些資源的引用,但又沒(méi)有及時(shí)釋放這些資源,就會(huì)導(dǎo)致內(nèi)存泄漏。
- 競(jìng)態(tài)條件:如果多個(gè)閉包同時(shí)訪問(wèn)和修改同一個(gè)共享變量,就可能出現(xiàn)競(jìng)態(tài)條件(race condition)的問(wèn)題。
- 函數(shù)返回值不確定:如果閉包引用了函數(shù)內(nèi)部的變量,那么當(dāng)函數(shù)返回后,在閉包被調(diào)用之前這些變量的值可能已經(jīng)被修改,從而導(dǎo)致閉包產(chǎn)生意外的行為。
3、怎么避免?
- 聲明新變量,閉包函數(shù)使用新變量
- 將變量通過(guò)函數(shù)參數(shù)形式傳遞進(jìn)閉包函數(shù)
20、結(jié)構(gòu)體比較
- 2 個(gè) interface 可以比較嗎 ?
??Go 語(yǔ)言中,接口(interface)是對(duì)非接口值(例如指針,struct 等)的封裝,內(nèi)部實(shí)現(xiàn)包含了 2 個(gè)字段,類型 T 和 值 V。接口類型的比較就演變成了結(jié)構(gòu)體比較。
??兩個(gè)接口類型比較時(shí),會(huì)先比較 T,再比較 V。接口類型與非接口類型比較時(shí),會(huì)先將非接口類型嘗試轉(zhuǎn)換為接口類型,再按接口比較的規(guī)則進(jìn)行比較。如果兩個(gè)接口變量底層類型和值完全相同(或同為 nil)則兩個(gè)變量相等,否則不等。
??接口類型比較時(shí),如果底層類型不可比較,則會(huì)發(fā)生 panic。
package mainimport "fmt"type Animal interface {Speak() string
}type Duck struct {Name string
}func (a Duck) Speak() string {return "I'm " + a.Name
}type Cat struct {Name string
}func (a Cat) Speak() string {return "I'm " + a.Name
}type Bird struct {Name stringSpeakFunc func() string
}func (a Bird) Speak() string {return "I'm " + a.SpeakFunc()
}// Animal 為接口類型,Duck 和 Cat 分別實(shí)現(xiàn)了該接口。
func main() {var d1, d2, c1 Animald1 = Duck{Name: "Donald Duck"}d2 = Duck{Name: "Donald Duck"}c1 = Cat{Name: "Donald Duck"}fmt.Println(d1 == d2) // 輸出 truefmt.Println(d1 == c1) // 輸出 false// 接口變量 d1、d2 底層類型同為 Duck 并且底層值相同,所以 d1 和 d2 相等。// 接口變量 c1 底層類型為 Cat,盡管底層值相同,但類型不同,c1 與 d1 也不相等。var animal Animalanimal = Duck{Name: "Donald Duck"}var duck Duckduck = Duck{Name: "Donald Duck"}fmt.Println(animal == duck) // 輸出 true// 當(dāng) struct 和接口進(jìn)行比較時(shí),可以簡(jiǎn)單地把 struct 轉(zhuǎn)換成接口然后再按接口比較的規(guī)則進(jìn)行判定。// animal 為接口變量,而 duck 為 struct 變量,底層類型同為 Duck 并且底層值相同,二者判定為相等。var b1 Animal = Bird{Name: "bird",SpeakFunc: func() string {return "I'm Poly"}}var b2 Animal = Bird{Name: "bird",SpeakFunc: func() string {return "I'm eagle"}}fmt.Println(b1 == b2)// panic: runtime error: comparing uncomparable type main.Bird// 結(jié)構(gòu)體 Bird 也實(shí)現(xiàn)了 Animal 接口,但結(jié)構(gòu)體中增加了一個(gè)不可比較的函數(shù)類型成員 SpeakFunc,// 因此 Bird 變成了不可比較類型,接口類型變量 b1 和 b2 底層類型為 Bird,在比較時(shí)會(huì)觸發(fā) panic。
}
- 2 個(gè) nil 可能不相等嗎?
??2 個(gè) nil 類型可能不相等,兩個(gè)nil 只有在類型相同時(shí)才相等。例如,interface 在運(yùn)行時(shí)綁定值,只有值為 nil 接口值才為 nil,但是與指針的 nil 不相等。
func main() {var p *int = nilvar i interface{}fmt.Println(p == nil) // 輸出 truefmt.Println(i == nil) // 輸出 truefmt.Println(i == p) // 輸出 false
}
- 結(jié)構(gòu)體比較
??結(jié)構(gòu)體是可以比較的,但前提是結(jié)構(gòu)體成員字段全部可以比較,并且結(jié)構(gòu)體成員字段類型、個(gè)數(shù)、順序也需要相同,當(dāng)結(jié)構(gòu)體成員全部相等時(shí),兩個(gè)結(jié)構(gòu)體相等。
??特別注意的點(diǎn),如果結(jié)構(gòu)體成員字段的順序不相同,那么結(jié)構(gòu)體也是不可以比較的。如果結(jié)構(gòu)體成員字段中有不可以比較的類型,如map、slice、function 等,那么結(jié)構(gòu)體也是不可以比較的。
func main() {sn1 := struct {age intname string}{age: 11, name: "Zhang San"}sn2 := struct {age intname string}{age: 11, name: "Zhang San"}fmt.Println(sn1 == sn2) // 輸出 truesn3 := struct {name stringage int}{age: 11, name: "Zhang San"}fmt.Println(sn1 == sn3)// 錯(cuò)誤提示:Invalid operation: sn1 == sn3 (mismatched types struct {...} and struct {...})sn4 := struct {name stringage intgrade map[string]int}{age: 11, name: "Zhang San"}sn5 := struct {name stringage intgrade map[string]int}{age: 11, name: "Zhang San"}fmt.Println(sn4 == sn5)// 錯(cuò)誤提示:Invalid operation: sn4 == sn5 (the operator == is not defined on struct {...})
}
21、go 語(yǔ)言中的可比較類型和不可比較類型
??比較操作符分為等值操作符(== 和 !=)和排序操作符(<、<=、> 、 >=),等值操作符作用的操作數(shù)必須是可比較的,排序操作符作用的操作數(shù)必須是可排序的。
操作符 | 變量類型 |
---|---|
等值操作符 (==、!=) | 整型、浮點(diǎn)型、字符串、布爾型、復(fù)數(shù)、指針、管道、接口、結(jié)構(gòu)體、數(shù)組 |
排序操作符 (<、<=、> 、 >=) | 整型、浮點(diǎn)型、字符串 |
不可比較類型 | map、slice、function |
??管道是可以比較的,管道本質(zhì)上是個(gè)指針,make 語(yǔ)句生成的是一個(gè)管道的指針,所以管道的比較規(guī)則與指針相同,兩個(gè)管道變量如果是同一個(gè) make 語(yǔ)句聲明(或同為 nil)則兩個(gè)管道相等,否則不等。
cha := make(chan int, 10)
chb := make(chan int, 10)
chc := cha
fmt.Println(cha == chb) // 輸出 false
fmt.Println(cha == chc) // 輸出 true
// 管道 cha 和 chb 雖然類型和空間完全相同,但由于出自不同的 make 語(yǔ)句,所以兩個(gè)管道不相等
// 但管道 chc 由于獲得了管道 cha 的地址,所以管道 cha 和 chc 相等fmt.Println(cha < chc)
// 錯(cuò)誤提示:Invalid operation: cha < chc (the operator < is not defined on chan int)
- map、slice、function 為什么不能直接用 == 比較?使用什么可以比較?
??至于這三種類型為什么不可比較,Golang 社區(qū)沒(méi)有給出官方解釋,經(jīng)過(guò)分析,可能是因?yàn)?比較的維度不好衡量,難以定義一種沒(méi)有爭(zhēng)議的比較規(guī)則。所以 go 官方并沒(méi)有定義比較運(yùn)算符(==和!=),而是只能與nil進(jìn)行比較。
??比如兩個(gè) slice 類型相同、長(zhǎng)度相同并且元素值也相同算不算相等?如果說(shuō)相等,那么如果兩個(gè) slice 地址不同,還算不算相等呢?答案就可能無(wú)法統(tǒng)一了。至于 map 也是同樣的道理。另外再看 function,兩個(gè)函數(shù)實(shí)現(xiàn)功能一樣,但實(shí)現(xiàn)邏輯不一樣算不算相等呢?可見(jiàn),這三種類型的比較容易引入歧義。
??使用 reflect.TypeOf(value).Comparable() 判斷可否進(jìn)行比較, 使用 reflect.DeepEqual(value 1, value 2) 進(jìn)行比較,當(dāng)然也有特殊情況,例如 []byte,通過(guò) bytes. Equal 函數(shù)進(jìn)行比較。但是反射非常影響性能。
func main() {s := "Hello World"aMap := make(map[string]int)bMap := make(map[string]int)fmt.Println(reflect.TypeOf(s).Comparable()) // 輸出 truefmt.Println(reflect.TypeOf(aMap).Comparable()) // 輸出 falsefmt.Println(reflect.TypeOf(bMap).Comparable()) // 輸出 falsefmt.Println(reflect.DeepEqual(aMap, bMap)) // 輸出 trueaMap["s"] = 1fmt.Println(reflect.DeepEqual(aMap, bMap)) // 輸出 false
}
22、interface 接口
-
在Go語(yǔ)言中,接口(interface)是方法的集合,它允許我們定義一組方法但不實(shí)現(xiàn)它們,任何類型只要實(shí)現(xiàn)了這些方法,就被認(rèn)為是實(shí)現(xiàn)了該接口。接口更重要的作用在于多態(tài)實(shí)現(xiàn),它允許程序以多態(tài)的方式處理不同類型的值。接口體現(xiàn)了程序設(shè)計(jì)的多態(tài)和高內(nèi)聚、低耦合的思想。
-
使用的注意事項(xiàng)
(1)interface 類型默認(rèn)是一個(gè)指針,引用類型。
(2)interface 接口不能包含任何變量。
(3)一個(gè)自定義類型可以實(shí)現(xiàn)多個(gè)接口。
(4)一個(gè)接口可以嵌套(繼承)多個(gè)別的接口。
(5)接口的實(shí)現(xiàn)是隱式的,不需要顯式聲明。
(6)空接口沒(méi)有任何方法,能被任意數(shù)據(jù)類型實(shí)現(xiàn)。
(7)結(jié)構(gòu)體類型(structs)、類型別名(type aliases)、其他接口、自定義類型、變量等都可以實(shí)現(xiàn)接口。 -
接口分為侵入式和非侵入式
??GO語(yǔ)言的接口是非侵入式接口。
??侵入式接口的實(shí)現(xiàn)是顯式聲明的,必須顯式的表明我要繼承那個(gè)接口,必須通過(guò)特定的關(guān)鍵字(?如Java中的implements)?來(lái)聲明實(shí)現(xiàn)關(guān)系。?
??非侵入式接口的實(shí)現(xiàn)是隱式聲明的,不需要通過(guò)任何關(guān)鍵字聲明類型與接口之間的實(shí)現(xiàn)關(guān)系。?只要一個(gè)類型實(shí)現(xiàn)了接口的所有方法,?那么這個(gè)類型就實(shí)現(xiàn)了這個(gè)接口。 -
應(yīng)用場(chǎng)景
Go接口的應(yīng)用場(chǎng)景包括多態(tài)性、?解耦、?擴(kuò)展性、?代碼復(fù)用、API設(shè)計(jì)、?單元測(cè)試、?插件系統(tǒng)、?依賴注入。?
類型轉(zhuǎn)換、類型判斷、實(shí)現(xiàn)多態(tài)功能、作為函數(shù)參數(shù)或返回值。 -
空接口的應(yīng)用場(chǎng)景
(1)用空接口可以讓函數(shù)和方法接受任意類型、任意數(shù)量的函數(shù)參數(shù),空接口切片還可以用于函數(shù)的可選參數(shù)。
(2)空接口還可以作為函數(shù)的返回值,但是極不推薦這樣干,因?yàn)榇a的維護(hù)、拓展與重構(gòu)將會(huì)變得極為痛苦。
(3)空接口可以實(shí)現(xiàn)保存任意類型值的字典 (map)。
23、空結(jié)構(gòu)體 struct 應(yīng)用場(chǎng)景
空結(jié)構(gòu)體在Go語(yǔ)言中通常用于需要一個(gè)空的值的場(chǎng)景。
- 作為channel的通知信號(hào)。通知型 channel,使用空結(jié)構(gòu)體當(dāng)做通知信號(hào),不會(huì)帶來(lái)額外的內(nèi)存開(kāi)銷。
- 作為map的值。map + 空 struct 實(shí)現(xiàn)集合 set,節(jié)省內(nèi)存。
- 作為方法接收器。在業(yè)務(wù)場(chǎng)景下,我們需要將方法組合起來(lái),代表其是一個(gè) ”分組“ 的,便于后續(xù)拓展和維護(hù)。在該場(chǎng)景下,使用空結(jié)構(gòu)體是最合適的,易拓展,省空間,最結(jié)構(gòu)化。
- 作為一個(gè)標(biāo)記或占位符。表示某個(gè)動(dòng)作已經(jīng)發(fā)生,某個(gè)元素已被處理。
- 作為接口的實(shí)現(xiàn)??战Y(jié)構(gòu)體可以實(shí)現(xiàn)一個(gè)或多個(gè)接口,而不需要存儲(chǔ)任何字段。這允許你創(chuàng)建符合特定接口的對(duì)象,而不需要為這些對(duì)象分配額外的內(nèi)存空間。這在某些設(shè)計(jì)模式(如工廠模式、單例模式等)中特別有用。
空結(jié)構(gòu)體主要有以下幾個(gè)特點(diǎn):
(1)零內(nèi)存占用 :空結(jié)構(gòu)體不占用任何內(nèi)存空間,?這使得空結(jié)構(gòu)體在內(nèi)存優(yōu)化方面非常有用。?
(2)地址相同:?無(wú)論創(chuàng)建多少個(gè)空結(jié)構(gòu)體,?它們所指向的地址都是相同的,這意味著所有空結(jié)構(gòu)體實(shí)例共享同一個(gè)內(nèi)存位置。
(3)無(wú)狀態(tài):?由于空結(jié)構(gòu)體沒(méi)有數(shù)據(jù)成員,?因此它不包含任何狀態(tài)信息。?
24、go 內(nèi)存泄漏
??內(nèi)存泄漏是指在程序執(zhí)行過(guò)程中,已不再使用的內(nèi)存空間沒(méi)有被及時(shí)釋放或者釋放時(shí)出現(xiàn)了錯(cuò)誤,導(dǎo)致這些內(nèi)存無(wú)法被使用,直到程序結(jié)束這些內(nèi)存才被釋放。
??如果出現(xiàn)內(nèi)存泄漏問(wèn)題,程序?qū)?huì)因?yàn)檎紦?jù)大量?jī)?nèi)存而變得異常緩慢,嚴(yán)重時(shí)可能會(huì)導(dǎo)致程序崩潰。在go語(yǔ)言中,可以通過(guò)runtime包里的freeosmemory()函數(shù)來(lái)進(jìn)行內(nèi)存回收和清理。此外,也可以使用一些工具來(lái)檢測(cè)內(nèi)存泄漏問(wèn)題,例如go pprof等。需要注意的是,內(nèi)存泄漏不是語(yǔ)言本身的問(wèn)題,而通常是程序編寫者忘記釋放內(nèi)存或者處理內(nèi)存時(shí)出現(xiàn)錯(cuò)誤導(dǎo)致的。
??在Go中內(nèi)存泄露分為暫時(shí)性內(nèi)存泄露和永久性內(nèi)存泄露。
- 暫時(shí)性內(nèi)存泄露
??臨時(shí)性泄露,指的是該釋放的內(nèi)存資源沒(méi)有及時(shí)釋放,對(duì)應(yīng)的內(nèi)存資源仍然有機(jī)會(huì)在更晚些時(shí)候被釋放,即便如此在內(nèi)存資源緊張情況下,也會(huì)是個(gè)問(wèn)題。這類主要是 string、slice 底層 buffer 的錯(cuò)誤共享,導(dǎo)致無(wú)用數(shù)據(jù)對(duì)象無(wú)法及時(shí)釋放,或者 defer 函數(shù)導(dǎo)致的資源沒(méi)有及時(shí)釋放。
1、獲取長(zhǎng)字符串中的一段導(dǎo)致長(zhǎng)字符串未釋放
2、獲取長(zhǎng)slice中的一段導(dǎo)致長(zhǎng)slice未釋放
3、獲取指針切片slice中的一段
4、defer 導(dǎo)致的內(nèi)存泄露
- 永久性內(nèi)存泄露
??永久性泄露,指的是在進(jìn)程后續(xù)生命周期內(nèi),泄露的內(nèi)存都沒(méi)有機(jī)會(huì)回收,如 goroutine 內(nèi)部預(yù)期之外的for-loop或者chan select-case導(dǎo)致的無(wú)法退出的情況,導(dǎo)致協(xié)程棧及引用內(nèi)存永久泄露問(wèn)題。
1、goroutine 協(xié)程阻塞,無(wú)法退出,導(dǎo)致內(nèi)存泄漏;
??Go運(yùn)行時(shí)不會(huì)殺死掛起的goroutines,因此分配給掛起goroutines的資源(以及所引用的內(nèi)存塊)永遠(yuǎn)不會(huì)被垃圾回收。
- channel 阻塞導(dǎo)致 goroutine 阻塞
- select 導(dǎo)致 goroutine 阻塞
- 互斥鎖沒(méi)有釋放,互斥鎖死鎖
- 申請(qǐng)過(guò)多的goroutine來(lái)不及釋放導(dǎo)致內(nèi)存泄漏
2、定時(shí)器使用不當(dāng),time.Ticker未關(guān)閉導(dǎo)致內(nèi)存泄漏;
time.After在定時(shí)器到達(dá)時(shí),會(huì)自動(dòng)內(nèi)回收。然后time.Ticker 鐘擺不使用時(shí),一定要Stop,不然會(huì)造成真內(nèi)存泄露。
3、不正確地使用終結(jié)器(Finalizers)導(dǎo)致內(nèi)存泄漏
25、協(xié)程泄露
??協(xié)程泄露是指協(xié)程創(chuàng)建后,長(zhǎng)時(shí)間得不到釋放,并且還在不斷地創(chuàng)建新的協(xié)程,最終導(dǎo)致內(nèi)存耗盡,程序崩潰。
??常見(jiàn)的導(dǎo)致協(xié)程泄露的場(chǎng)景有以下幾種:
??1、缺少接收方,導(dǎo)致發(fā)送方阻塞。啟動(dòng)1000個(gè)協(xié)程向信道發(fā)送數(shù)字,但只接收了一次,導(dǎo)致 999 個(gè)協(xié)程阻塞,不能退出。
??2、缺少發(fā)送方,導(dǎo)致接收方阻塞。啟動(dòng) 1000 個(gè)協(xié)程接收信道的信息,但信道并不會(huì)發(fā)送那么多次的信息,也會(huì)導(dǎo)致接收協(xié)程被阻塞,不能退出。
??3、死鎖。多個(gè)協(xié)程由于競(jìng)爭(zhēng)資源或者彼此通信而造成阻塞,不能退出。
??4、select操作。select里也是channel操作,如果所有case上的操作阻塞,且沒(méi)有default分支進(jìn)行處理,goroutine也無(wú)法繼續(xù)執(zhí)行。
26、值傳遞和地址傳遞(引用傳遞)
??Go語(yǔ)言函數(shù)傳參時(shí),默認(rèn)使用值傳遞。
??值傳遞是指當(dāng)我們調(diào)用一個(gè)方法并將參數(shù)傳遞給它時(shí),實(shí)際上是把變量的一個(gè)副本傳遞給了函數(shù),而非原始變量自己,兩個(gè)變量的地址不同,不可相互修改。 在函數(shù)內(nèi)部,對(duì)于這個(gè)參數(shù)所做的任何更改,只會(huì)影響副本的值,不會(huì)影響原始變量。
??地址傳遞(引用傳遞)是指?jìng)鬟f給函數(shù)的是變量的指針或者地址(變量本身),函數(shù)可以通過(guò)修改這個(gè)地址上的值來(lái)更改變量的值,對(duì)該變量的修改在所有使用它的地方都是可見(jiàn)的。
區(qū)別 | 值傳遞 | 引用傳遞 |
---|---|---|
傳參 | 變量的副本 | 變量本身(指針或者地址) |
影響范圍 | 只影響副本,不影響原始變量 | 影響原始變量 |
使用場(chǎng)景 | 處理較小的變量時(shí)速度更快 | 避免復(fù)制大塊的內(nèi)存內(nèi)容 |
27、go 語(yǔ)言中棧的空間有多大?
??多數(shù)架構(gòu)上默認(rèn)棧大小都在 2 ~ 4 MB 左右。在Go語(yǔ)言中,??臻g大小并不是固定的,?而是根據(jù)程序的運(yùn)行需求動(dòng)態(tài)調(diào)整的,?Goroutine 的初始棧大小降低到了 2KB。在64位操作系統(tǒng)上,?Go中的??臻g最大可以擴(kuò)展到1GB。?
早期版本的Go可能將最小棧內(nèi)存設(shè)置為4KB或8KB,而后來(lái)為了優(yōu)化性能,又可能將其調(diào)整回2KB。
28、并發(fā)情況下的數(shù)據(jù)處理,避免并發(fā)情況下數(shù)據(jù)競(jìng)爭(zhēng)問(wèn)題?
??1、使用互斥鎖(mutex):在操作共享資源之前,加鎖;完成操作后,解鎖。
??2、使用讀寫互斥鎖(RWMutex):讀不阻塞,寫阻塞。允許多個(gè)goroutine同時(shí)訪問(wèn)共享變量,寫入時(shí)就必須等到其他goroutine不再讀寫了才能進(jìn)行。這種方法適合在讀多寫少的場(chǎng)景。
??3、使用通道(channel)串行化操作:使用通道來(lái)實(shí)現(xiàn)數(shù)據(jù)同步和互斥訪問(wèn),將需要訪問(wèn)的數(shù)據(jù)發(fā)送到一個(gè)通道中,可以保證同一時(shí)間只有一個(gè)goroutine能夠訪問(wèn)該數(shù)據(jù)。類似于生產(chǎn)者消費(fèi)者模式。
??4、使用原子操作(atomic):如果只是對(duì)一個(gè)共享變量進(jìn)行簡(jiǎn)單的增加或減少,就可以使用原子操作。原子操作是一個(gè)CPU指令,它保證在一個(gè)時(shí)刻內(nèi)執(zhí)行所有的增量或減量,避免了并發(fā)情況下的數(shù)據(jù)競(jìng)爭(zhēng)問(wèn)題。
二、channel 通道
??在Go語(yǔ)言中,channel是一種用于在goroutine之間傳遞數(shù)據(jù)的安全通信機(jī)制。它可以被看做是一種特殊類型的隊(duì)列,其中的數(shù)據(jù)只能被一個(gè)goroutine讀取而另一個(gè)goroutine寫入。要?jiǎng)?chuàng)建一個(gè)channel,可以使用內(nèi)置的make函數(shù)。
1、底層數(shù)據(jù)結(jié)構(gòu)
type hchan struct {// chan 里元素?cái)?shù)量qcount uint// chan 底層循環(huán)數(shù)組的長(zhǎng)度dataqsiz uint// 指向底層循環(huán)數(shù)組的指針// 只針對(duì)有緩沖的 channelbuf unsafe.Pointer// chan 中元素大小elemsize uint16// chan 是否被關(guān)閉的標(biāo)志closed uint32// chan 中元素類型elemtype *_type // element type// 已發(fā)送元素在循環(huán)數(shù)組中的索引sendx uint // send index// 已接收元素在循環(huán)數(shù)組中的索引recvx uint // receive index// 等待接收的 goroutine 隊(duì)列recvq waitq // list of recv waiters// 等待發(fā)送的 goroutine 隊(duì)列sendq waitq // list of send waiters// 保護(hù) hchan 中所有字段lock mutex
}
從channel中讀數(shù)據(jù):
1、若等待發(fā)送隊(duì)列 sendq 不為空,且沒(méi)有緩沖區(qū),直接從 sendq 中取出 G ,把 G 中數(shù)據(jù)讀出,最后把 G 喚醒,結(jié)束讀取過(guò)程。
2、如果等待發(fā)送隊(duì)列 sendq 不為空,說(shuō)明緩沖區(qū)已滿,從緩沖區(qū)中首部讀出數(shù)據(jù),把 G 中數(shù)據(jù)寫入緩沖區(qū)尾部,把 G 喚醒,結(jié)束讀取過(guò)程。
3、如果緩沖區(qū)中有數(shù)據(jù),則從緩沖區(qū)取出數(shù)據(jù),結(jié)束讀取過(guò)程。將當(dāng)前 goroutine 加入 recvq ,進(jìn)入睡眠,等待被寫 goroutine 喚醒。
往channel中寫數(shù)據(jù)
1、若等待接收隊(duì)列 recvq 不為空,則緩沖區(qū)中無(wú)數(shù)據(jù)或無(wú)緩沖區(qū),將直接從 recvq 取出 G ,并把數(shù)據(jù)寫入,最后把該 G 喚醒,結(jié)束發(fā)送過(guò)程。
2、若緩沖區(qū)中有空余位置,則將數(shù)據(jù)寫入緩沖區(qū),結(jié)束發(fā)送過(guò)程。
3、若緩沖區(qū)中沒(méi)有空余位置,則將發(fā)送數(shù)據(jù)寫入 G,將當(dāng)前 G 加入 sendq ,進(jìn)入睡眠,等待被讀 goroutine 喚醒。
關(guān)閉 channel
關(guān)閉 channel 時(shí)會(huì)將 recvq 中的 G 全部喚醒,本該寫入 G 的數(shù)據(jù)位置為 nil 。將 sendq 中的 G 全部喚醒,但是這些 G 會(huì) panic。
2、channel為什么能做到線程安全?
??channel可以理解是一個(gè)先進(jìn)先出的循環(huán)隊(duì)列,通過(guò)管道進(jìn)行通信,發(fā)送一個(gè)數(shù)據(jù)到Channel和從Channel接收一個(gè)數(shù)據(jù)都是原子性的。不要通過(guò)共享內(nèi)存來(lái)通信,而是通過(guò)通信來(lái)共享內(nèi)存,前者就是傳統(tǒng)的加鎖,后者就是Channel。設(shè)計(jì)Channel的主要目的就是在多任務(wù)間傳遞數(shù)據(jù)的,本身就是安全的。
3、無(wú)緩沖的 channel 和 有緩沖的 channel 的區(qū)別?
??阻塞與否是分別針對(duì)發(fā)送方、接收方而言的,可以類比生產(chǎn)者與消費(fèi)者問(wèn)題。
??對(duì)于無(wú)緩沖的 channel,發(fā)送方將阻塞該信道,直到接收方從該信道接收到數(shù)據(jù)為止,而接收方也將阻塞該信道,直到發(fā)送方將數(shù)據(jù)發(fā)送到該信道中為止。
??對(duì)于有緩存的 channel,發(fā)送方在緩沖區(qū)滿的時(shí)候阻塞,接收方不阻塞;接收方在緩沖區(qū)為空的時(shí)候阻塞,發(fā)送方不阻塞。
4、channel 死鎖的場(chǎng)景
1、當(dāng)一個(gè)channel中沒(méi)有數(shù)據(jù),而直接讀取時(shí),會(huì)發(fā)生死鎖;
2、當(dāng)channel數(shù)據(jù)滿了,再嘗試寫數(shù)據(jù)會(huì)造成死鎖;
3、向一個(gè)關(guān)閉的channel寫數(shù)據(jù)。
??解決方案是采用select語(yǔ)句,再default放默認(rèn)處理方式。
1、Go的select語(yǔ)句是一種僅能用于channl發(fā)送和接收消息的專用語(yǔ)句,此語(yǔ)句運(yùn)行期間是阻塞的;當(dāng)select中沒(méi)有case語(yǔ)句的時(shí)候,會(huì)阻塞當(dāng)前groutine。
2、select是Golang在語(yǔ)言層面提供的I/O多路復(fù)用的機(jī)制,其專門用來(lái)檢測(cè)多個(gè)channel是否準(zhǔn)備完畢:可讀或可寫。
3、select語(yǔ)句中除default外,每個(gè)case操作一個(gè)channel,要么讀要么寫。
4、select語(yǔ)句中除default外,各case執(zhí)行順序是隨機(jī)的。
5、select語(yǔ)句中如果沒(méi)有default語(yǔ)句,則會(huì)阻塞等待任一case。
6、select語(yǔ)句中讀操作要判斷是否成功讀取,關(guān)閉的channel也可以讀取。
5、操作 channel 的情況總結(jié)
操作 | nil channel(未初始化) | closed channel | not nil, not closed channel |
---|---|---|---|
close | panic | panic | 正常關(guān)閉 |
讀 <- ch | 阻塞 | 讀到對(duì)應(yīng)類型的零值 | 阻塞或正常讀取數(shù)據(jù)。非緩沖型 channel 沒(méi)有等待發(fā)送者或緩沖型 channel buf為空時(shí)會(huì)阻塞 |
寫 ch <- | 阻塞 | panic | 阻塞或正常寫入數(shù)據(jù)。非緩沖型 channel 沒(méi)有等待接收者或緩沖型 channel buf 滿時(shí)會(huì)被阻塞 |
總結(jié)一下,發(fā)生 panic 的情況有三種:向一個(gè)關(guān)閉的 channel 進(jìn)行寫操作;關(guān)閉一個(gè)未初始化的 channel;重復(fù)關(guān)閉一個(gè) channel。
讀、寫一個(gè)未初始化的channel 都會(huì)被阻塞。
三、map 哈希表
1、map 的底層數(shù)據(jù)結(jié)構(gòu)是什么?
?? 源碼位于 src\runtime\map.go 中。
??golang 中 map 底層使用的是哈希查找表,用鏈表來(lái)解決哈希沖突。每個(gè) map 的底層結(jié)構(gòu)是 hmap,是有若干個(gè)結(jié)構(gòu)為 bmap 的 bucket 組成的數(shù)組,每個(gè) bucket 底層都采用鏈表結(jié)構(gòu)。
hmap的結(jié)構(gòu):
type hmap struct {count int // map中元素的數(shù)量,調(diào)用len()直接返回此值flags uint8 // 狀態(tài)標(biāo)識(shí)符,key和value是否包指針、是否正在擴(kuò)容、是否已經(jīng)被迭代B uint8 // map中桶數(shù)組的數(shù)量,桶數(shù)組的長(zhǎng)度的對(duì)數(shù),len(buckets) == 2^B,可以最多容納 6.5 * 2 ^ B 個(gè)元素,6.5為裝載因子noverflow uint16 // 溢出桶的大概數(shù)量,當(dāng)B小于16時(shí)是準(zhǔn)確值,大于等于16時(shí)是大概的值hash0 uint32 // 哈希種子,用于計(jì)算哈希值,為哈希函數(shù)的結(jié)果引入一定的隨機(jī)性,降低哈希沖突的概率buckets unsafe.Pointer // 指向桶數(shù)組的指針,長(zhǎng)度為 2^B ,如果元素個(gè)數(shù)為0,就為 niloldbuckets unsafe.Pointer // 指向一個(gè)舊桶數(shù)組,用于擴(kuò)容,它的長(zhǎng)度是當(dāng)前桶數(shù)組的一半nevacuate uintptr // 搬遷進(jìn)度,小于此地址的桶數(shù)組遷移完成extra *mapextra // 可選字段,用于gc,指向所有的溢出桶,避免gc時(shí)掃描整個(gè)map,僅掃描所有溢出桶就足夠了
}
bmap的結(jié)構(gòu):
type bmap struct {tophash [bucketCnt]uint8 // bucketCnt=8,存放key哈希值的高8位,用于決定kv鍵值對(duì)放在桶內(nèi)的哪個(gè)位置
}
??buckets是一個(gè)bmap數(shù)組,數(shù)組的長(zhǎng)度就是 2^B。每個(gè)bucket固定包含8個(gè)key和value,實(shí)現(xiàn)上面是一個(gè)固定的大小連續(xù)內(nèi)存塊,分成四部分:tophash 值,8個(gè)key值,8個(gè)value值,指向下個(gè)bucket的指針。
??tophash 值用于快速查找key是否在該bucket中,當(dāng)插入和查詢運(yùn)行時(shí)都會(huì)使用哈希哈數(shù)對(duì)key做哈希運(yùn)算,獲取一個(gè)hashcode,取高8位存放在bmap tophash字段中。
??桶結(jié)構(gòu)中,所有的key放一起,所有的value放一起,而不是key/value一對(duì)一存放,目的是省去 pad 字段,節(jié)省內(nèi)存空間。由于內(nèi)存對(duì)齊的原因,key/value一對(duì)一的形式可能需要更多的補(bǔ)齊空間。
??每個(gè) bucket 設(shè)計(jì)成最多只能放 8 個(gè) key-value 對(duì),如果有第 9 個(gè) key-value 落入當(dāng)前的 bucket,那就需要再構(gòu)建一個(gè)溢出桶,通過(guò)指針連接起來(lái)。
key 定位過(guò)程
??key 經(jīng)過(guò)哈希計(jì)算后得到哈希值,共 64 個(gè) bit 位(64位機(jī)),
??低 B 位:后 B 位,決定在哪個(gè)桶。用于尋找當(dāng)前key屬于哪個(gè)bucket,桶的編號(hào);
??高 8 位:前 8 位,決定在桶的的哪個(gè)位置。找到此 key 在 bucket 中的位置,第幾個(gè)槽位,key 和 value 也對(duì)應(yīng)第幾個(gè)。最開(kāi)始桶內(nèi)還沒(méi)有 key,新加入的 key 會(huì)找到第一個(gè)空位,放入。
2、map的擴(kuò)容
1、裝載因子(平均每個(gè)桶存儲(chǔ)的元素個(gè)數(shù))
??Go的裝載因子閾值常量:6.5,map 最多可容納 6.5*2^B 個(gè)元素。
??裝載因子等于 map中元素的個(gè)數(shù) / map的容量,即len(map) / 2^B。裝載因子用來(lái)表示空閑位置的情況,裝載因子越大,表明空閑位置越少,沖突也越多,散列表的性能會(huì)下降。
為什么裝載因子是6.5?不是8?不是1?
??裝載因子是哈希表中的一個(gè)重要指標(biāo),主要目的是為了平衡 buckets 的存儲(chǔ)空間大小和查找元素時(shí)的性能高低。
??Go 官方發(fā)現(xiàn):裝載因子越大,填入的元素越多,空間利用率就越高,但發(fā)生沖突的幾率就變大;反之,裝數(shù)因子越小,填入的元素越少,沖突發(fā)生的幾率減小,但空間利用率低,而且還會(huì)提高擴(kuò)容操作的次數(shù)。根據(jù)測(cè)試結(jié)果和討論,Go 官方取了一個(gè)相對(duì)適中的值6.5。
2、觸發(fā) map 擴(kuò)容的時(shí)機(jī)(插入、刪除key):
??觸發(fā)擴(kuò)容的時(shí)機(jī)是新增操作,搬遷的時(shí)機(jī)是賦值和刪除操作,每次最多搬遷兩個(gè)bucket。
擴(kuò)容分為等量擴(kuò)容和2倍增量擴(kuò)容。
條件1:
??當(dāng)元素個(gè)數(shù)超過(guò)負(fù)載,元素個(gè)數(shù) > 6.5 * 桶個(gè)數(shù),擴(kuò)容一倍,屬于增量擴(kuò)容;
條件2:
??當(dāng)使用的溢出桶過(guò)多時(shí),重新分配一樣大的內(nèi)存空間,屬于等量擴(kuò)容;(實(shí)際上沒(méi)有擴(kuò)容,主要是為了回收空閑的溢出桶,提高 map 的查找和插入效率)
如何定義溢出桶是否太多需要等量擴(kuò)容呢?兩種情況:
- 當(dāng)B小于15時(shí),溢出桶的數(shù)量超過(guò)2^B桶總數(shù),屬于溢出桶數(shù)量太多,需要等量擴(kuò)容;
- 當(dāng)B大于等于15時(shí),溢出桶數(shù)量超過(guò)2^15,屬于溢出桶數(shù)量太多,需要等量擴(kuò)容。
??條件2是對(duì)條件1的補(bǔ)充,例如不停地插入、刪除元素,導(dǎo)致創(chuàng)建很多的溢出桶,但裝載因子不高,達(dá)不到條件1的臨界值,不能觸發(fā)擴(kuò)容來(lái)緩解這種情況。溢出桶數(shù)量太多,桶使用率低,導(dǎo)致 key 會(huì)很分散,查找插入效率低,空間利用率低。
3、擴(kuò)容策略(怎么擴(kuò)容?)
??Go 會(huì)創(chuàng)建一個(gè)新的 buckets 數(shù)組,新的 buckets 數(shù)組的容量是舊buckets數(shù)組的兩倍(或者和舊桶容量相同),將原始桶數(shù)組中的所有元素重新散列到新的桶數(shù)組中。這樣做的目的是為了使每個(gè)桶中的元素?cái)?shù)量盡可能平均分布,以提高查詢效率。舊的buckets數(shù)組不會(huì)被直接刪除,而是會(huì)把原來(lái)對(duì)舊數(shù)組的引用去掉,讓GC來(lái)清除內(nèi)存。
??擴(kuò)容過(guò)程是漸進(jìn)式的,主要是防止一次擴(kuò)容要搬遷的元素太多引發(fā)性能問(wèn)題。
??在map進(jìn)行擴(kuò)容遷移的期間,不會(huì)觸發(fā)第二次擴(kuò)容。只有在前一個(gè)擴(kuò)容遷移工作完成后,map才能進(jìn)行下一次擴(kuò)容操作。
4、搬遷策略
??由于map擴(kuò)容需要將原有的kv鍵值對(duì)搬遷到新的內(nèi)存地址,如果一下子全部搬完,會(huì)非常的影響性能。go 中 map 的擴(kuò)容采用漸進(jìn)式的搬遷策略,原有的 key 并不會(huì)一次性搬遷完畢,一次性搬遷會(huì)造成比較大的延時(shí),每次最多只會(huì)搬遷 2 個(gè) bucket,將搬遷的O(N)開(kāi)銷均攤到O(1)的賦值和刪除操作上。
??插入或修改、刪除 key 的時(shí)候,都會(huì)嘗試進(jìn)行搬遷 buckets 的工作。
3、從未初始化的 map 讀數(shù)據(jù)會(huì)發(fā)生什么?
??從未初始化的 map 讀取數(shù)據(jù),則會(huì)返回該值類型的零值。當(dāng)給未初始化的 map 賦值時(shí),會(huì)出現(xiàn)運(yùn)行時(shí)錯(cuò)誤 “panic: assignment to entry in nil map” ,這是因?yàn)槲唇?jīng)初始化的 map 是一個(gè) nil 值,并且不能對(duì) nil map 進(jìn)行賦值操作。因此,在使用一個(gè) map 之前,必須確保它已經(jīng)正確地初始化。
操作 | nil map (未初始化) | 空 map (長(zhǎng)度為 0) |
---|---|---|
賦值 | panic | 不會(huì)報(bào)錯(cuò) |
打印 | 不會(huì)報(bào)錯(cuò),打印 map[] | 不會(huì)報(bào)錯(cuò) |
讀取 | 不會(huì)報(bào)錯(cuò),讀到對(duì)應(yīng)類型的零值 | 不會(huì)報(bào)錯(cuò) |
刪除 | 不會(huì)報(bào)錯(cuò) | 不會(huì)報(bào)錯(cuò) |
func main() {var a map[int]int // 未初始化 mapb := map[int]int{} // 空 mapa[1] = 1 // panic: assignment to entry in nil mapb[1] = 1fmt.Println(a) // map[]fmt.Println(b) // map[1:1]fmt.Println(a[1]) // 0fmt.Println(b[1]) // 1delete(a, 1)delete(b, 1)
}
4、map 中的 key 為什么是無(wú)序的?怎么實(shí)現(xiàn)有序?
無(wú)序的原因
??(1)map底層的擴(kuò)容與搬遷:map在擴(kuò)容后,會(huì)發(fā)生key的搬遷,原來(lái)在同一個(gè)桶的key,搬遷后,有可能就不處于同一個(gè)桶了,而遍歷map的過(guò)程,就是遍歷這些桶,桶里的元素發(fā)生了變化,map遍歷當(dāng)然就是無(wú)序的。
??(2)隨機(jī) bucket 隨機(jī)序號(hào):Go 中遍歷 map 時(shí),并不是固定地從 0 號(hào) bucket 開(kāi)始遍歷,每次都是從一個(gè)隨機(jī)值序號(hào)的 bucket 開(kāi)始遍歷,并且是從這個(gè) bucket 的一個(gè)隨機(jī)序號(hào)的 cell 開(kāi)始遍歷。這樣,即使你是一個(gè)寫死的 map,僅僅只是遍歷它,也不太可能會(huì)返回一個(gè)固定序列的 key/value 對(duì)了。
實(shí)現(xiàn)有序
??(1)使用第三方庫(kù),如 github.com/iancoleman/orderedmap 或者 github.com/wkhere/ordered_map,這些庫(kù)提供了類似于標(biāo)準(zhǔn)庫(kù)中的 map 操作,同時(shí)也保持了元素的順序。
??(2)使用 sort 包。使key有序,對(duì)key排序,再遍歷key輸出value;使value有序,用struct存放key和value,實(shí)現(xiàn)sort接口,調(diào)用sort.Sort進(jìn)行排序。
5、map并發(fā)訪問(wèn)安全嗎?怎么解決?可以邊遍歷邊刪除嗎?
??map 在并發(fā)情況下,只讀是線程安全的,同時(shí)讀寫是線程不安全的。在并發(fā)訪問(wèn)下,多個(gè)goroutine同時(shí)讀寫同一個(gè)map會(huì)導(dǎo)致數(shù)據(jù)競(jìng)爭(zhēng)(data race)問(wèn)題,這可能導(dǎo)致不可預(yù)期的結(jié)果和程序崩潰。并發(fā)讀寫的時(shí)候運(yùn)行時(shí)會(huì)有檢查,在查找、賦值、遍歷、刪除的過(guò)程中都會(huì)進(jìn)行寫保護(hù)檢測(cè),檢測(cè)寫標(biāo)志,一旦發(fā)現(xiàn)寫標(biāo)志置位(等于1),則直接 panic。賦值和刪除函數(shù)在檢測(cè)完寫標(biāo)志是復(fù)位之后,先將寫標(biāo)志位置位,才會(huì)進(jìn)行之后的操作。
??go 官方認(rèn)為,map 更應(yīng)適配典型使用場(chǎng)景(不需要從多個(gè) goroutine 中進(jìn)行安全訪問(wèn)),而不是為了小部分情況(并發(fā)訪問(wèn)),導(dǎo)致大部分程序付出加鎖的代價(jià),影響性能,所以決定了不支持。
解決方法:
(1)使用讀寫鎖 sync.RWMutex,讀之前調(diào)用 RLock() 函數(shù),讀完之后調(diào)用 RUnlock() 函數(shù)解鎖;寫之前調(diào)用 Lock() 函數(shù),寫完之后,調(diào)用 Unlock() 解鎖。
(2)使用 sync.Map ,并發(fā)安全的 map。使用 Store() 函數(shù)賦值,使用 Load() 函數(shù)獲取值。
Go map和sync.Map誰(shuí)的性能好,為什么?
??sync.Map 性能好,空間換時(shí)間機(jī)制,冗余的數(shù)據(jù)結(jié)構(gòu)就是dirty和read,發(fā)生鎖競(jìng)爭(zhēng)的頻率小,減少了加鎖對(duì)性能的影響。適合讀多寫少的場(chǎng)景,寫多的場(chǎng)景,需要加鎖,性能會(huì)下降。
遍歷操作:只需遍歷read即可,而read是并發(fā)讀安全的,沒(méi)有鎖,相比于加鎖方案,性能大為提升
查找操作:先在read中查找,read中找不到再去dirty中找
邊遍歷邊刪除——同時(shí)讀寫?
(1)多個(gè)協(xié)程同時(shí)讀寫同一個(gè) map 會(huì)直接 panic。
(2)如果在同一個(gè)協(xié)程內(nèi)邊遍歷邊刪除,并不會(huì) panic,但是,遍歷的結(jié)果就可能不會(huì)是相同的了,有可能結(jié)果集中包含了刪除的 key,也有可能不包含,這取決于刪除 key 的時(shí)間:是在遍歷到 key 所在的 bucket 時(shí)刻前或者后。
6、map元素可以取地址嗎?
??無(wú)法對(duì) map 的 key 或 value 進(jìn)行取址,因?yàn)閿U(kuò)容后map元素的地址會(huì)發(fā)生變化,歸根結(jié)底還是map底層的擴(kuò)容與搬遷。
7、map 中刪除一個(gè) key,它的內(nèi)存會(huì)釋放么?
??不會(huì)釋放,因?yàn)閯h除只是將桶對(duì)應(yīng)位置的tophash置空而已,如果kv存儲(chǔ)的是指針,那么會(huì)清理指針指向的內(nèi)存,否則不會(huì)真正回收內(nèi)存,內(nèi)存占用并不會(huì)減少。
??在大多數(shù)情況下,刪除 Map 中的 key 不會(huì)立即釋放內(nèi)存。這是因?yàn)樵诖蠖鄶?shù)語(yǔ)言中,Map 內(nèi)部實(shí)現(xiàn)使用哈希表或紅黑樹(shù)等數(shù)據(jù)結(jié)構(gòu)來(lái)存儲(chǔ)鍵值對(duì),而刪除一個(gè)鍵值對(duì)只是將該鍵值對(duì)的引用從內(nèi)部數(shù)據(jù)結(jié)構(gòu)中刪除,并不會(huì)立即釋放與其相關(guān)的內(nèi)存。
8、什么樣的類型可以做 map 的鍵 key?
??Go 語(yǔ)言中只要是可比較的類型都可以作為 key。除開(kāi) slice,map,functions 這幾種類型,其他類型都是 OK 的。具體包括:布爾值、數(shù)字、字符串、指針、通道、接口類型、結(jié)構(gòu)體、只包含上述類型的數(shù)組。這些類型的共同特征是支持 == 和 != 操作符,k1 == k2 時(shí),可認(rèn)為 k1 和 k2 是同一個(gè) key。如果是結(jié)構(gòu)體,只有 hash 后的值相等以及字面值相等,才被認(rèn)為是相同的 key。很多字面值相等的,hash出來(lái)的值不一定相等,比如引用。
??任何類型都可以作為 value,包括 map 類型。
float 型可以作為 key,但是由于精度的問(wèn)題,會(huì)導(dǎo)致一些詭異的問(wèn)題,慎用之。
2.4 == 2.4000000000000000000000001
當(dāng)用 float64 作為 key 的時(shí)候,先要將其轉(zhuǎn)成 unit64 類型,再插入 key 中。2.4 和 2.4000000000000000000000001 經(jīng)過(guò) math.Float64bits() 函數(shù)轉(zhuǎn)換后的結(jié)果是一樣的,所以認(rèn)為同一個(gè) key 。
NAN != NAN
hash(NAN) != hash(NAN)
NAN 是從一個(gè)常量解析得來(lái)的,為什么插入 map 時(shí),會(huì)被認(rèn)為是不同的 key?
哈希函數(shù)針對(duì) NAN,會(huì)再加一個(gè)隨機(jī)數(shù),所以認(rèn)為是不同的key。
9、如何比較兩個(gè) map 相等?
map 深度相等的條件:
1、都為 nil
2、非空、長(zhǎng)度相等,指向同一個(gè) map 實(shí)體對(duì)象
3、相應(yīng)的 key 指向的 value “深度”相等
??直接將使用 map1 == map2 是錯(cuò)誤的,編譯不通過(guò)。== 只能比較 map 是否為 nil。因此只能是遍歷map 的每個(gè)元素,比較元素是否都是深度相等。使用 reflect.DeepEqual 進(jìn)行比較。
10、map怎么解決哈希沖突?
??在Go語(yǔ)言中,map是通過(guò)哈希表來(lái)實(shí)現(xiàn)的,當(dāng)多個(gè)鍵映射到哈希表的同一個(gè)桶時(shí),就會(huì)發(fā)生哈希沖突。Go語(yǔ)言使用 鏈地址法(拉鏈法)解決哈希沖突。
解決哈希沖突的方法:
- 開(kāi)放尋址法
??如果發(fā)生哈希沖突,從發(fā)生沖突的那個(gè)單元起,按一定的次序,不斷重復(fù),從哈希表中尋找一個(gè)空閑的單元,將該鍵值對(duì)存儲(chǔ)在該單元中。具體的實(shí)現(xiàn)方式包括線性探測(cè)法、平方探測(cè)法、隨機(jī)探測(cè)法和雙重哈希法等。開(kāi)放尋址法需要的表長(zhǎng)度要大于等于所需要存放的元素?cái)?shù)量。- 鏈地址法(拉鏈法)
??基于數(shù)組 + 鏈表 實(shí)現(xiàn)哈希表,數(shù)組中每個(gè)元素都是一個(gè)鏈表,將每個(gè)桶都指向一個(gè)鏈表,當(dāng)哈希沖突發(fā)生時(shí),新的鍵值對(duì)會(huì)按順序添加到該桶對(duì)應(yīng)的鏈表的尾部。在查找特定鍵值對(duì)時(shí),可以遍歷該鏈表以查找與之匹配的鍵值對(duì)。
兩種方案的比較:
(1)對(duì)于鏈地址法,基于 數(shù)組 + 鏈表 進(jìn)行存儲(chǔ),鏈表節(jié)點(diǎn)可以在需要時(shí)再創(chuàng)建,開(kāi)放尋址法需要事先申請(qǐng)好足夠內(nèi)存,因此鏈地址法對(duì)內(nèi)存的利用率高。
(2)鏈地址法對(duì)裝載因子的容忍度會(huì)更高,適合存儲(chǔ)大對(duì)象、大數(shù)據(jù)量的哈希表,而且相較于開(kāi)放尋址法,它更加靈活,支持更多的優(yōu)化策略,比如可采用紅黑樹(shù)代替鏈表。但是鏈地址法需要額外的空間來(lái)存儲(chǔ)指針。
(3)對(duì)于開(kāi)放尋址法,它只有數(shù)組一種數(shù)據(jù)結(jié)構(gòu)就可完成存儲(chǔ),繼承了數(shù)組的優(yōu)點(diǎn),對(duì)CPU緩存友好,易于序列化操作,但是它對(duì)內(nèi)存的利用率不高,且發(fā)生沖突時(shí)代價(jià)更高。當(dāng)數(shù)據(jù)量明確、裝載因子小,適合采用開(kāi)放尋址法。
11、map 使用中注意的點(diǎn)?
(1)一定要先初始化,再使用,否則panic;
(2)map 不是線程安全的;
(3)map 的 key 必須是可比較的;
(4)map是無(wú)序的。
12、map 創(chuàng)建、賦值、刪除、查詢的過(guò)程?
??寫保護(hù)檢測(cè),查找、賦值、遍歷、刪除的過(guò)程中都會(huì)檢測(cè)寫標(biāo)志位 flags,一旦發(fā)現(xiàn) flags 的寫標(biāo)志位被置為1,則直接 panic,因?yàn)檫@表明有其他協(xié)程同時(shí)在進(jìn)行寫操作。查找,賦值,刪除這些操作一個(gè)很核心的內(nèi)容都是如何定位key的位置。
創(chuàng)建 map
??創(chuàng)建 map 底層調(diào)用的是 makemap() 函數(shù),主要做的工作就是初始化 hmap 結(jié)構(gòu)體的各種字段,例如計(jì)算 B 的大小、設(shè)置哈希種子 hash0 、分配桶空間等。
默認(rèn)會(huì)創(chuàng)建2^B個(gè)bucket,如果b大于等于4,會(huì)預(yù)先創(chuàng)建一些溢出桶,b小于4的情況可能用不到溢出桶,沒(méi)必要預(yù)先創(chuàng)建
??其中的關(guān)鍵點(diǎn)在于哈希函數(shù)的選擇,在程序啟動(dòng)時(shí),會(huì)檢測(cè) cpu 是否支持 aes,如果支持,則使用 aes hash,否則使用 memhash。這是在函數(shù) alginit() 中完成,位于路徑:src/runtime/alg.go 下。
hash 函數(shù),有加密型和非加密型。
加密型的一般用于加密數(shù)據(jù)、數(shù)字摘要等,典型代表就是 md5、sha1、sha256、aes256 這種;
非加密型的一般就是查找。在 map 的應(yīng)用場(chǎng)景中,用的是查找。
選擇 hash 函數(shù)主要考察的是兩點(diǎn):性能、碰撞概率。
map 的賦值(修改)過(guò)程
??向 map 中插入或者修改 key,底層調(diào)用的是 mapassign() 函數(shù),根據(jù) key 類型的不同,編譯器會(huì)將其優(yōu)化為相應(yīng)的“快速函數(shù)”。流程:對(duì) key 計(jì)算 hash 值,根據(jù) hash 值按照之前的流程,找到要賦值的位置(可能是插入新 key,也可能是更新老 key),對(duì)相應(yīng)位置進(jìn)行賦值。
??核心還是一個(gè)雙層循環(huán),外層遍歷 bucket 和它的 overflow bucket,內(nèi)層遍歷整個(gè) bucket 的各個(gè) cell。
map 的刪除過(guò)程
??底層調(diào)用的是 mapdelete() 函數(shù),根據(jù) key 類型的不同,刪除操作會(huì)被優(yōu)化成更具體的函數(shù)。它首先會(huì)檢查 h.flags 標(biāo)志,如果發(fā)現(xiàn)寫標(biāo)位是 1,直接 panic,因?yàn)檫@表明有其他協(xié)程同時(shí)在進(jìn)行寫操作。計(jì)算 key 的哈希值,找到落入的 bucket。檢查此 map 如果正在擴(kuò)容的過(guò)程中,直接觸發(fā)一次搬遷操作。
??刪除操作同樣是兩層循環(huán),核心還是找到 key 的具體位置。尋找過(guò)程都是類似的,在 bucket 中挨個(gè) cell 尋找。找到對(duì)應(yīng)位置后,對(duì) key 或者 value 進(jìn)行“清零”操作。最后,將 count 值減 1,將對(duì)應(yīng)位置的 tophash 值置成 Empty。
刪除key僅僅只是將其對(duì)應(yīng)的tohash值置空,如果kv存儲(chǔ)的是指針,那么會(huì)清理指針指向的內(nèi)存,否則不會(huì)真正回收內(nèi)存,內(nèi)存占用并不會(huì)減少。
如果正在擴(kuò)容,并且操作的bucket沒(méi)有搬遷完,那么會(huì)搬遷bucket。
map 的查詢過(guò)程
??底層調(diào)用的是 mapaccess1()、mapaccess2() 函數(shù),mapaccess2() 函數(shù)返回值多了一個(gè) bool 型變量,兩者的代碼也是完全一樣的,只是在返回值后面多加了一個(gè) false 或者 true。根據(jù) key 的不同類型,編譯器用更具體的函數(shù)替換,以優(yōu)化效率。流程:計(jì)算hash值并根據(jù)hash值找到桶,遍歷桶和桶串聯(lián)的溢出桶,尋找 key。
??需要注意的地方:如果根據(jù)hash值定位到桶正在進(jìn)行搬遷,并且這個(gè)bucket還沒(méi)有搬遷到新桶中,那么就從老的桶中找。在bucket中進(jìn)行順序查找,使用高八位進(jìn)行快速過(guò)濾,高八位相等,再比較key是否相等,找到就返回value。如果當(dāng)前bucket找不到,就往下找溢出桶,都沒(méi)有就返回零值。
四、slice 切片
1、數(shù)組和切片的區(qū)別
- 相同點(diǎn):
(1)都是只能存儲(chǔ)一組相同類型的數(shù)據(jù)結(jié)構(gòu);
(2)下標(biāo)都是從0開(kāi)始的,可以通過(guò)下標(biāo)來(lái)訪問(wèn)單個(gè)元素;
(3)有容量、長(zhǎng)度,長(zhǎng)度通過(guò) len 獲取,容量通過(guò) cap 獲取。
- 不同點(diǎn):
(1)數(shù)組是定長(zhǎng)的,長(zhǎng)度定義好之后,不能再更改,長(zhǎng)度是類型的一部分,訪問(wèn)和復(fù)制不能超過(guò)數(shù)組定義的長(zhǎng)度,否則就會(huì)下標(biāo)越界。切片長(zhǎng)度和容量可以自動(dòng)擴(kuò)容,切片的類型和長(zhǎng)度無(wú)關(guān)。
在 Go 中,數(shù)組是不常見(jiàn)的,因?yàn)槠溟L(zhǎng)度是類型的一部分,限制了它的表達(dá)能力,比如 [3]int 和 [4]int 就是不同的類型。
(2)數(shù)組是值類型。切片是引用類型,每個(gè)切片都引用了一個(gè)底層數(shù)組,切片本身不能存儲(chǔ)任何數(shù)據(jù),都是底層數(shù)組存儲(chǔ)數(shù)據(jù),修改切片的時(shí)候修改的是底層數(shù)組中的數(shù)據(jù),切片一旦擴(kuò)容,會(huì)指向一個(gè)新的底層數(shù)組,內(nèi)存地址也就隨之改變。
2、slice 底層數(shù)據(jù)結(jié)構(gòu)
?? 源碼位于 src\runtime\slice.go 中。
??golang 中 slice 實(shí)際上是一個(gè)結(jié)構(gòu)體,包含三個(gè)字段:長(zhǎng)度、容量、底層數(shù)組。
type slice struct {array unsafe.Pointer // 指向底層數(shù)組的指針len int // 長(zhǎng)度 cap int // 容量
}
??注意,底層數(shù)組是可以被多個(gè) slice 同時(shí)指向的,因此對(duì)一個(gè) slice 的元素進(jìn)行操作是有可能影響到其他 slice 的。
??創(chuàng)建 slice 底層調(diào)用的是 makeslice() 函數(shù),主要工作是向 Go 內(nèi)存管理器申請(qǐng)內(nèi)存(在堆上分配),返回指向底層數(shù)組的指針。
32KB = 32768 字節(jié)。小對(duì)象是從per-P緩存的空閑列表中分配的。大型對(duì)象(>32kB)是直接從堆中分配的。
??擴(kuò)容,底層調(diào)用的是 growslice() 函數(shù),里面包括擴(kuò)容規(guī)則、內(nèi)存對(duì)齊、申請(qǐng)新內(nèi)存、拷貝舊數(shù)據(jù)。
??拷貝,底層調(diào)用的是 slicecopy() 函數(shù),切片中全部元素通過(guò)memmove或者數(shù)組指針的方式將整塊內(nèi)存中的內(nèi)容拷貝到目標(biāo)的內(nèi)存區(qū)域,所以大切片拷貝需要注意性能影響,不過(guò)比一個(gè)個(gè)的復(fù)制要有更好的性能。
3、slice 的擴(kuò)容
1、觸發(fā)擴(kuò)容的時(shí)機(jī)
??向 slice 追加元素,如果底層數(shù)組的容量不夠(即便底層數(shù)組并未填滿),就會(huì)觸發(fā)擴(kuò)容。追加元素調(diào)用的是 append 函數(shù)。
2、擴(kuò)容規(guī)則
Go <= 1.17
1、首先判斷,如果新申請(qǐng)容量(cap)大于2倍的舊容量(old.cap),最終容量(newcap)就是新申請(qǐng)的容量(cap)。
2、否則判斷,如果舊切片的長(zhǎng)度小于1024,則最終容量(newcap)就是舊容量(old.cap)的 2 倍。
3、否則判斷,如果舊切片長(zhǎng)度大于等于1024,則最終容量(newcap)就是舊容量(old.cap)按照 1.25 倍循環(huán)遞增,也就是每次加上 cap / 4。
4、如果最終容量(cap)計(jì)算值溢出,則最終容量(cap)就是新申請(qǐng)容量(cap)。
Go1.18之后
??引入了新的擴(kuò)容規(guī)則,首先 1024 的邊界不復(fù)存在,取而代之的常量是 256 。超出256的情況,也不是直接擴(kuò)容25%,而是設(shè)計(jì)了一個(gè)平滑過(guò)渡的計(jì)算方法,隨著容量增大,擴(kuò)容比例逐漸從100%平滑降低到25%,從 2 倍平滑過(guò)渡到 1.25 倍。
為什么要這樣設(shè)計(jì)?
??避免追加過(guò)程中頻繁擴(kuò)容,減少內(nèi)存分配和數(shù)據(jù)復(fù)制開(kāi)銷,有助于性能提升。
3、內(nèi)存對(duì)齊
??計(jì)算出了新容量之后,還沒(méi)有完,出于內(nèi)存的高效利用考慮,還要進(jìn)行內(nèi)存對(duì)齊。進(jìn)行內(nèi)存對(duì)齊之后,新 slice 的容量是要 大于等于 老 slice 容量的 2倍或者1.25倍。
4、完整過(guò)程
??向 slice 追加元素的時(shí)候,若容量不夠,會(huì)觸發(fā)擴(kuò)容,會(huì)調(diào)用 growslice 函數(shù)。首先,根據(jù)擴(kuò)容規(guī)則,計(jì)算出新的容量,然后進(jìn)行內(nèi)存對(duì)齊,之后,向 Go 內(nèi)存管理器申請(qǐng)內(nèi)存,將老 slice 中的數(shù)據(jù)整個(gè)復(fù)制過(guò)去,并且將追加的元素添加到新的底層數(shù)組中。
4、slice 的拷貝
1、淺拷貝
??淺拷貝,拷貝的是地址,淺拷貝只復(fù)制了指向底層數(shù)據(jù)結(jié)構(gòu)的指針,而不是復(fù)制整個(gè)底層數(shù)據(jù)結(jié)構(gòu),修改新對(duì)象的值會(huì)影響原對(duì)象值。對(duì)于引用類型,如切片和字典等都是淺拷貝。
slice2 := slice1
??slice1和slice2指向的都是同一個(gè)底層數(shù)組,任何一個(gè)數(shù)組元素被改變,都可能會(huì)影響兩個(gè)slice。在slice觸發(fā)擴(kuò)容操作前,slice1和slice2指向的都是相同數(shù)組,但在觸發(fā)擴(kuò)容操作后,二者指向的就不一定是相同的底層數(shù)組了。
2、深拷貝
??深拷貝,拷貝的是數(shù)據(jù)本身,完全復(fù)制了底層數(shù)據(jù)結(jié)構(gòu),而不是復(fù)制指向底層數(shù)據(jù)結(jié)構(gòu)的指針,會(huì)創(chuàng)建一個(gè)新對(duì)象,新對(duì)象和原對(duì)象不共享內(nèi)存,它們是完全獨(dú)立的,修改新對(duì)象的值不會(huì)影響原對(duì)象值,內(nèi)存地址不同,釋放內(nèi)存地址時(shí),可以分別釋放。
copy(slice2, slice1)
??把 slice1 的數(shù)據(jù)復(fù)制到 slice2 中,修改 slice2 的數(shù)據(jù),不會(huì)影響到 slice1 。如果 slice2 的長(zhǎng)度和容量小于 slice1 的,那么只會(huì)復(fù)制 slice2 長(zhǎng)度的數(shù)據(jù)。
5、append 函數(shù)
??使用 append 可以向 slice 追加元素,實(shí)際上是往底層數(shù)組添加元素,如果底層數(shù)組的容量不夠,會(huì)觸發(fā)擴(kuò)容。append 函數(shù)的參數(shù)長(zhǎng)度可變,因此可以追加多個(gè)值到 slice 中,還可以用 … 傳入 slice,直接追加一個(gè)切片。
??append函數(shù)返回值是一個(gè)新的slice,Go編譯器不允許調(diào)用了 append 函數(shù)后不使用返回值。
??注意:append不會(huì)修改傳參進(jìn)來(lái)的slice(len和cap),只會(huì)在不夠用的時(shí)候新分配一個(gè)array,并把之前的slice依賴的array數(shù)據(jù)拷貝過(guò)來(lái);所以對(duì)同一個(gè)slice 重復(fù) append,只要不超過(guò)cap,都是修改的同一個(gè)array,后面的會(huì)覆蓋前面。
func main() {a := []int{1, 2, 3, 4, 5}b := append(a, 100)fmt.Println(b) // [1 2 3 4 5 100]c := append(a, 200)fmt.Println(c) // [1 2 3 4 5 200]
}
6、切片作為函數(shù)參數(shù)?
??當(dāng) slice 作為函數(shù)參數(shù)時(shí),是值傳遞,函數(shù)內(nèi)部對(duì) slice 的作用并不會(huì)改變外層的 slice ,要想真的改變外層 slice,只有將返回的新的 slice 賦值到原始 slice,或者向函數(shù)傳遞一個(gè)指向 slice 的指針。slice 結(jié)構(gòu)體自身不會(huì)被改變,指針指向的底層數(shù)組的地址也不會(huì)被改變,改變的是數(shù)組中的數(shù)據(jù)。
??傳slice和傳slice的引用,其實(shí)開(kāi)銷區(qū)別不大。Go 語(yǔ)言的函數(shù)參數(shù)傳遞,只有值傳遞,沒(méi)有引用傳遞。
7、切片 slice 使用時(shí)注意的點(diǎn)?
(1)創(chuàng)建slice時(shí)應(yīng)根據(jù)實(shí)際需要預(yù)分配容量,避免追加過(guò)程中頻繁擴(kuò)容,有助于性能提升;在大批量添加數(shù)據(jù)時(shí),建議?次性分配足夠大的空間,以減少內(nèi)存分配和數(shù)據(jù)復(fù)制開(kāi)銷;
(2)slice是非并發(fā)安全的,如要實(shí)現(xiàn)并發(fā)安全,請(qǐng)采用鎖或channle;
(3)大數(shù)組作為函數(shù)參數(shù)時(shí),會(huì)復(fù)制整個(gè)數(shù)組,消耗過(guò)多內(nèi)存,建議采用slice或指針;
(4)如果只用到大的slice或數(shù)組的一部分,建議將需要部分復(fù)制到新的slice中取,以便釋放大的slice底層數(shù)組內(nèi)存,減少內(nèi)存占用;
(5)多個(gè)slice指向相同的底層數(shù)組時(shí),修改其中一個(gè)slice,可能會(huì)影響其他slice的值;
(6)slice作為參數(shù)傳遞時(shí),比數(shù)組更為高效,因?yàn)閟lice本身的結(jié)構(gòu)就比較小,所以你參數(shù)傳遞時(shí),傳slice和傳slice的引用,其實(shí)開(kāi)銷區(qū)別不大;
(7)slice在擴(kuò)容時(shí),可能會(huì)發(fā)生底層數(shù)組的變更和數(shù)據(jù)拷貝;
(8)及時(shí)釋放不再使用的 slice 對(duì)象,避免持有過(guò)期數(shù)組,造成 GC 無(wú)法回收。
8、slice 內(nèi)存泄露情況
??當(dāng) slice2 的底層數(shù)組很大,但 slice1 使用 slice2 中很小的一段,slice1 和 slice2 共用一個(gè)底層數(shù)組,底層數(shù)組占據(jù)的大部分空間都是被浪費(fèi)的,沒(méi)法被回收,造成了內(nèi)存泄露。
解決方法:
??不再引用 slice2 數(shù)組,將需要的數(shù)據(jù)復(fù)制到一個(gè)新的slice中,這樣新slice的底層數(shù)組,就和 slice2 數(shù)組無(wú)任何關(guān)系了。
func main() {slice2 := make([]int, 1000)// 錯(cuò)誤用法slice1 := slice2[:1] // slice1 和 slice2 共用一個(gè)底層數(shù)組// 正確用法copy(slice1, slice2[:1])return
}
9、slice 并發(fā)不安全
??在Go語(yǔ)言中,slice是并發(fā)不安全的,主要有以下兩個(gè)原因:數(shù)據(jù)競(jìng)爭(zhēng)、內(nèi)存重分配。slice底層的結(jié)構(gòu)體包含一個(gè)指向底層數(shù)組的指針和該數(shù)組的長(zhǎng)度,當(dāng)多個(gè)協(xié)程并發(fā)訪問(wèn)同一個(gè)slice時(shí),有可能會(huì)出現(xiàn)數(shù)據(jù)競(jìng)爭(zhēng)的問(wèn)題。例如,一個(gè)協(xié)程在修改slice的長(zhǎng)度,而另一個(gè)協(xié)程同時(shí)在讀取或修改slice的內(nèi)容。在向slice中追加元素時(shí),可能會(huì)觸發(fā)slice的擴(kuò)容操作,在這個(gè)過(guò)程中,如果有其他協(xié)程訪問(wèn)了slice,就會(huì)導(dǎo)致指向底層數(shù)組的指針出現(xiàn)異常。
??要并發(fā)安全,有兩種方法:加互斥鎖、使用channel串行化操作。加互斥鎖適合于對(duì)性能要求不高的場(chǎng)景,畢竟鎖的粒度太大,這種方式屬于通過(guò)共享內(nèi)存來(lái)實(shí)現(xiàn)通信。channle 適合于對(duì)性能要求大的場(chǎng)景,channle就是專用于goroutine間通信的,這種方式屬于通過(guò)通信來(lái)實(shí)現(xiàn)共享內(nèi)存。
10、從未初始化的 slice讀數(shù)據(jù)會(huì)發(fā)生什么?
??從未初始化的 slice 上讀取數(shù)據(jù),給未初始化的 slice 賦值,會(huì)發(fā)生運(yùn)行時(shí)錯(cuò)誤(panic),未初始化的 slice 沒(méi)有分配底層數(shù)組,指向底層數(shù)組的指針為 nil,因此不能存儲(chǔ)元素,讀取和賦值會(huì) panic: runtime error: index out of range [0] with length 0。因此,在使用一個(gè)slice之前,必須確保它已經(jīng)正確地初始化。