面試官:說下Golang Slice的底層實(shí)現(xiàn),淚崩了!
1、Go 語言當(dāng)中數(shù)組和切片的區(qū)別是什么?
數(shù)組:
數(shù)組固定長(zhǎng)度數(shù)組長(zhǎng)度是數(shù)組類型的一部分,所以[3]int 和[4]int 是兩種不同 的數(shù)組類型數(shù)組需要指定大小,不指定也會(huì)根據(jù)處初始化對(duì)的自動(dòng)推算出大 小,不可改變數(shù)組是通過值傳遞的
切片: 切片可以改變長(zhǎng)度切片是輕量級(jí)的數(shù)據(jù)結(jié)構(gòu),三個(gè)屬性,指針,長(zhǎng)度,容量不 需要指定大小切片是地址傳遞(引用傳遞)可以通過數(shù)組來初始化,也可以通 過內(nèi)置函數(shù) make()來初始化,初始化的時(shí)候 len=cap,然后進(jìn)行擴(kuò)容。
2、Go 語言當(dāng)中值傳遞和地址傳遞(引用傳遞)如何運(yùn)用?有什么區(qū)別?
?值傳遞只會(huì)把參數(shù)的值復(fù)制一份放進(jìn)對(duì)應(yīng)的函數(shù),兩個(gè)變量的地址不同, 不可相互修改。
地址傳遞(引用傳遞)會(huì)將變量本身傳入對(duì)應(yīng)的函數(shù),在函數(shù)中可以對(duì)該變 量進(jìn)行值內(nèi)容的修改。
3、Go 語言當(dāng)中數(shù)組和切片在傳遞的時(shí)候的區(qū)別是什么?
數(shù)組是值傳遞
切片是引用傳遞
func?main()?{
????arr?:=?make([]int,?0)
????for?i?:=?0;?i?2000;?i++?{
fmt.Println("len?為",?len(arr),?"cap?為",?cap(arr))
????????arr?=?append(arr,?i)
????}
}
我們可以看下結(jié)果
依次是0,1,2,4,8,16,32,64,128,256,512,1024
但到了 1024 之后,就變成了1024,1280,1696,2304
每次都是擴(kuò)容了四分之一左右
5、看下面代碼的 defer 的執(zhí)行順序是什么? defer 的作用和特點(diǎn)是什么?
defer 的作用是:
你只需要在調(diào)用普通函數(shù)或方法前加上關(guān)鍵字 defer,就完成了 defer 所需要
的語法。當(dāng) defer 語句被執(zhí)行時(shí),跟在 defer 后面的函數(shù)會(huì)被延遲執(zhí)行。直到
包含該 defer 語句的函數(shù)執(zhí)行完畢時(shí),defer 后的函數(shù)才會(huì)被執(zhí)行,不論包含defer 語句的函數(shù)是通過 return 正常結(jié)束,還是由于 panic 導(dǎo)致的異常結(jié)束。你可以在一個(gè)函數(shù)中執(zhí)行多條 defer 語句,它們的執(zhí)行順序與聲明順序相反。
defer 的常用場(chǎng)景:
defer語句經(jīng)常被用于處理成對(duì)的操作,如打開、關(guān)閉、連接、斷開連接、加鎖、釋放鎖。
通過defer機(jī)制,不論函數(shù)邏輯多復(fù)雜,都能保證在任何執(zhí)行路徑下,資
源被釋放。
釋放資源的defer應(yīng)該直接跟在請(qǐng)求資源的語句后。
切片是基于數(shù)組實(shí)現(xiàn)的,它的底層是數(shù)組,它自己本身非常小,可以理解為對(duì)底層數(shù)組的抽象。因?yàn)榛跀?shù)組實(shí)現(xiàn),所以它的底層的內(nèi)存是連續(xù)分配的,效率非常高,還可以通過索引獲得數(shù)據(jù),可以迭代以及垃圾回收優(yōu)化。
切片本身并不是動(dòng)態(tài)數(shù)組或者數(shù)組指針。它內(nèi)部實(shí)現(xiàn)的數(shù)據(jù)結(jié)構(gòu)通過指針引用 底層數(shù)組,設(shè)定相關(guān)屬性將數(shù)據(jù)讀寫操作限定在指定的區(qū)域內(nèi)。切片本身是一 個(gè)只讀對(duì)象,其工作機(jī)制類似數(shù)組指針的一種封裝。
切片對(duì)象非常小,是因?yàn)樗侵挥?3 個(gè)字段的數(shù)據(jù)結(jié)構(gòu):
指向底層數(shù)組的指針
切片的長(zhǎng)度
切片的容量
7、Golang Slice 的擴(kuò)容機(jī)制,有什么注意點(diǎn)?
Go 中切片擴(kuò)容的策略是這樣的:
首先判斷,如果新申請(qǐng)容量大于2倍的舊容量,最終容量就是新申請(qǐng)的容 量
否則判斷,如果舊切片的長(zhǎng)度小于1024,則最終容量就是舊容量的兩倍
否則判斷,如果舊切片長(zhǎng)度大于等于1024,則最終容量從舊容量開始循環(huán)
增加原來的 1/4, 直到最終容量大于等于新申請(qǐng)的容量
如果最終容量計(jì)算值溢出,則最終容量就是新申請(qǐng)容量
8、擴(kuò)容前后的 Slice 是否相同?
情況一:
原數(shù)組還有容量可以擴(kuò)容(實(shí)際容量沒有填充完),這種情況下,擴(kuò)容以后的 數(shù)組還是指向原來的數(shù)組,對(duì)一個(gè)切片的操作可能影響多個(gè)指針指向相同地址 的 Slice。
情況二:
原來數(shù)組的容量已經(jīng)達(dá)到了最大值,再想擴(kuò)容, Go 默認(rèn)會(huì)先開一片內(nèi)存區(qū)
域,把原來的值拷貝過來,然后再執(zhí)行 append() 操作。這種情況絲毫不影響
原數(shù)組。
要復(fù)制一個(gè) Slice,最好使用 Copy 函數(shù)。
9、Golang 的參數(shù)傳遞、引用類型
Go 語言中所有的傳參都是值傳遞(傳值),都是一個(gè)副本,一個(gè)拷貝。因?yàn)榭?貝的內(nèi)容有時(shí)候是非引用類型(int、string、struct 等這些),這樣就在函 數(shù)中就無法修改原內(nèi)容數(shù)據(jù);有的是引用類型(指針、map、slice、chan等 這些),這樣就可以修改原內(nèi)容數(shù)據(jù)。
Golang 的引用類型包括 slice、map 和 channel。它們有復(fù)雜的內(nèi)部結(jié)構(gòu),除 了申請(qǐng)內(nèi)存外,還需要初始化相關(guān)屬性。內(nèi)置函數(shù) new 計(jì)算類型大小,為其分 配零值內(nèi)存,返回指針。而 make 會(huì)被編譯器翻譯成具體的創(chuàng)建函數(shù),由其分 配內(nèi)存和初始化成員結(jié)構(gòu),返回對(duì)象而非指針。
10、Golang Map 底層實(shí)現(xiàn)
Golang 中 map 的底層實(shí)現(xiàn)是一個(gè)散列表,因此實(shí)現(xiàn) map 的過程實(shí)際上就是實(shí)現(xiàn) 散表的過程。在這個(gè)散列表中,主要出現(xiàn)的結(jié)構(gòu)體有兩個(gè),一個(gè)叫 hmap(a header for a go map),一個(gè)叫 bmap(a bucket for a Go map,通常叫其bucket)。
11、Golang Map 如何擴(kuò)容
裝載因子:count/2^B
觸發(fā)條件:
1. 裝填因子是否大于6.5
2. overflow bucket 是否太多
解決方法:
1. 雙倍擴(kuò)容:擴(kuò)容采取了一種稱為“漸進(jìn)式”地方式,原有的 key 并不會(huì)一
次性搬遷完畢,每次最多只會(huì)搬遷 2 個(gè) bucket
2. 等量擴(kuò)容:重新排列,極端情況下,重新排列也解決不了,map成了鏈表,
性能大大降低,此時(shí)哈希種子 hash0 的設(shè)置,可以降低此類極端場(chǎng)景的發(fā) 生。
12、Golang Map 查找
Go 語言中 map 采用的是哈希查找表,由一個(gè) key 通過哈希函數(shù)得到哈希值,64位系統(tǒng)中就生成一個(gè) 64bit 的哈希值,由這個(gè)哈希值將 key 對(duì)應(yīng)到不同的桶 (bucket)中,當(dāng)有多個(gè)哈希映射到相同的的桶中時(shí),使用鏈表解決哈希沖 突。key 經(jīng)過 hash 后共 64 位,根據(jù) hmap 中 B 的值,計(jì)算它到底要落在哪個(gè)桶 時(shí),桶的數(shù)量為 2^B,如 B=5,那么用 64 位最后 5 位表示第幾號(hào)桶,在用 hash值的高 8 位確定在 bucket 中的存儲(chǔ)位置,當(dāng)前 bmap 中的 bucket 未找到,則查 詢對(duì)應(yīng)的 overflow bucket,對(duì)應(yīng)位置有數(shù)據(jù)則對(duì)比完整的哈希值,確定是否 是要查找的數(shù)據(jù)。
如果兩個(gè)不同的 key 落在的同一個(gè)桶上,hash 沖突使用鏈表法接近,遍歷bucket 中的 key 如果當(dāng)前處于 map 進(jìn)行了擴(kuò)容,處于數(shù)據(jù)搬移狀態(tài),則優(yōu)先從oldbuckets 查找。
13、介紹一下 Channel
Go 語言中,不要通過共享內(nèi)存來通信,而要通過通信來實(shí)現(xiàn)內(nèi)存共享。Go 的CSP(Communicating Sequential Process)并發(fā)模型,中文可以叫做通信順序進(jìn)
程,是通過 goroutine 和 channel 來實(shí)現(xiàn)的。
所以 channel 收發(fā)遵循先進(jìn)先出 FIFO,分為有緩存和無緩存,channel 中大致
有 buffer(當(dāng)緩沖區(qū)大小部位 0 時(shí),是個(gè) ring buffer)、sendx 和 recvx 收發(fā)
的位置(ring buffer 記錄實(shí)現(xiàn))、sendq、recvq 當(dāng)前 channel 因?yàn)榫彌_區(qū)不足
而阻塞的隊(duì)列、使用雙向鏈表存儲(chǔ)、還有一個(gè) mutex 鎖控制并發(fā)、其他原屬
等。
14、Go 語言的 Channel 特性?
給一個(gè) nil channel 發(fā)送數(shù)據(jù),造成永遠(yuǎn)阻塞
從一個(gè) nil channel 接收數(shù)據(jù),造成永遠(yuǎn)阻塞
給一個(gè)已經(jīng)關(guān)閉的 channel 發(fā)送數(shù)據(jù),引起 panic
從一個(gè)已經(jīng)關(guān)閉的 channel 接收數(shù)據(jù),如果緩沖區(qū)中為空,則返回一個(gè)零值
無緩沖的channel是同步的,而有緩沖的channel是非同步的
關(guān)閉一個(gè) nil channel 將會(huì)發(fā)生 panic
15、Channel 的 ring buffer 實(shí)現(xiàn)
channel 中使用了 ring buffer(環(huán)形緩沖區(qū)) 來緩存寫入的數(shù)據(jù)。ring buffer 有很多好處,而且非常適合用來實(shí)現(xiàn) FIFO 式的固定長(zhǎng)度隊(duì)列。在 channel 中,ring buffer 的實(shí)現(xiàn)如下:

hchan 中有兩個(gè)與 buffer 相關(guān)的變量:recvx 和 sendx。其中 sendx 表示buffer 中可寫的 index,recvx 表示 buffer 中可讀的 index。從 recvx 到sendx 之間的元素,表示已正常存放入 buffer 中的數(shù)據(jù)。
我們可以直接使用 buf[recvx]來讀取到隊(duì)列的第一個(gè)元素,使用 buf[sendx] =
x 來將元素放到隊(duì)尾。
最后:
點(diǎn)擊下方名片鏈接,關(guān)注 「碼農(nóng)編程進(jìn)階筆記?」微信公眾號(hào),在微信聊天對(duì)話框回復(fù)「go電子書」「黑馬Go語言」或者直接長(zhǎng)按左下圖海報(bào)中的二維碼,可獲取最新golang電子書和視頻資源
