本文轉載 https://zhuanlan。zhihu。com/p/519979757 作者:滬猿小韓
1、申明
文章部分題目來源於網路,答案繫個人結合5月份面試了近30家公司整理所得,最後附錄參考原文連結,如有遺漏的原文出處請聯絡本人。不對之處望批評指正,答案需要加上自己的思考,最好是程式碼實踐下。
參與過面試的企業有:zg人壽,睿科lun,qi貓,yun漢商城,zi節跳動,特斯la,蝦P,chuan音,qi安信,ai立信等大大小小企業近30家,BAT簡歷都過不了。
如果最後對你有幫助,幫忙點個關注!
2、面試建議
技術部分
1)、演算法部分,刷LeetCode就完事了,這是一個長期的過程,短期突擊沒啥效果,因為題目太多了。
2)、語言基礎,細分為:golang的基礎及原理,就是本文主要內容了;mysql基礎及原理;redis基礎及原理;kafka或其他訊息中介軟體(如果沒用過,需要了解大概的底層原理及結構);linux常用的命令,比如定時指令碼幾個引數時間分別代表啥,檔案許可權需要搞清楚,程序記憶體佔用命令;小公司還要懂一些前端的知識,因為他們希望你什麼都會。
3)、專案經驗,可以搞一個基於gin的後端介面服務的web框架,一般會問你怎麼實現的;以及微服務瞭解一下。
非技術部分
1)因為上海5月份居家辦公,遠端面試,這些題目準備一份,遇到卡殼的題目完全可以參考你準備好的答案,因為影片面試你眼睛是看著面試官還是題目是不太容易區分的(把題目視窗置頂)。
2)HR面也可以完全準備一份可能問到的問題的答案,並不是說你不會回答,而是會讓你的表達更順暢,其次也說明你是有備而來的,我在某拉公司面試就吃了這個虧,技術透過,HR說我的表達能力不行(後續我也會把這個模板分享出來,感謝我媳婦充當面試官,以及指導如何高情商的回答HR的問題)。
3)可以自己錄音面試回答,看看自己的語氣、音量,順暢度,如果自己聽了都不舒服,面試官可能也不舒服。
一、基礎部分
1、golang 中 make 和 new 的區別?(基本必問)
共同點:
給變數分配記憶體
不同點:
1)作用變數型別不同,new給string, int和陣列分配記憶體,make給切片,map,channel分配記憶體;
2)返回型別不一樣,new返回指向變數的指標,make返回變數本身;
3)new 分配的空間被清零。make 分配空間後,會進行初始化;
4) 位元組的面試官還說了另外一個區別,就是分配的位置,在堆上還是在棧上?這塊我比較模糊,大家可以自己探究下,我搜索出來的答案是golang會弱化分配的位置的概念,因為編譯的時候會自動記憶體逃逸處理,懂的大佬幫忙補充下:make、new記憶體分配是在堆上還是在棧上?
2、陣列和切片的區別 (基本必問)
相同點:
1)只能儲存一組相同型別的資料結構
2)都是透過下標來訪問,並且有容量長度,長度透過 len 獲取,容量透過 cap 獲取
區別:
1)陣列是定長,訪問和複製不能超過陣列定義的長度,否則就會下標越界,切片長度和容量可以自動擴容
2)陣列是值型別,切片是引用型別,每個切片都引用了一個底層陣列,切片本身不能儲存任何資料,都是這底層陣列儲存資料,所以修改切片的時候修改的是底層陣列中的資料。切片一旦擴容,指向一個新的底層陣列,記憶體地址也就隨之改變
簡潔地回答:
1)定義方式不一樣
2)初始化方式不一樣,陣列需要指定大小,大小不能改變
3)在函式傳遞中,陣列切片都是值傳遞。
陣列的定義
var a1 [3]int
var a2 [。。。]int{1,2,3}
切片的定義
var a1 []int
var a2 :=make([]int, 3, 5)
陣列的初始化
a1 := [。。。]int{1,2,3}
a2 := [5]int{1,2,3}
切片的初始化
b:= make([]int, 3, 5)
3、for range 的時候它的地址會發生變化麼?
答:在 for a,b := range c 遍歷中, a 和 b 在記憶體中只會存在一份,即之後每次迴圈時遍歷到的資料都是以值覆蓋的方式賦給 a 和 b,a,b 記憶體地址始終不變。由於有這個特性,for 迴圈裡面如果開協程,不要直接把 a 或者 b 的地址傳給協程。解決辦法:在每次迴圈時,建立一個臨時變數。
4、go defer,多個 defer 的順序,defer 在什麼時機會修改返回值?
作用:defer延遲函式,釋放資源,收尾工作;如釋放鎖,關閉檔案,關閉連結;捕獲panic;
避坑指南:defer函式緊跟在資源開啟後面,否則defer可能得不到執行,導致記憶體洩露。
多個 defer 呼叫順序是 LIFO(後入先出),defer後的操作可以理解為壓入棧中
defer,return,return value(函式返回值) 執行順序:首先return,其次return value,最後defer。defer可以修改函式最終返回值,修改時機:
有名返回值或者函式返回指標
參考:
【Golang】Go語言defer用法大總結(含return返回機制)__乳酪的部落格-CSDN部落格
有名返回值
func b() (i int) { defer func() { i++ fmt。Println(“defer2:”, i) }() defer func() { i++ fmt。Println(“defer1:”, i) }() return i //或者直接寫成return}func main() { fmt。Println(“return:”, b())}
函式返回指標
func c() *int { var i int defer func() { i++ fmt。Println(“defer2:”, i) }() defer func() { i++ fmt。Println(“defer1:”, i) }() return &i}func main() { fmt。Println(“return:”, *(c()))}
5、uint 型別溢位問題
超過最大儲存值如uint8最大是255
var a uint8 =255
var b uint8 =1
a+b = 0總之型別溢位會出現難以意料的事
6、能介紹下 rune 型別嗎?
相當int32
golang中的字串底層實現是透過byte陣列的,中文字元在unicode下佔2個位元組,在utf-8編碼下佔3個位元組,而golang預設編碼正好是utf-8
byte 等同於int8,常用來處理ascii字元
rune 等同於int32,常用來處理unicode或utf-8字元
7、 golang 中解析 tag 是怎麼實現的?反射原理是什麼?(中高階肯定會問,比較難,需要自己多去總結)
type User struct { name string `json:name-field` age int}func main() { user := &User{“John Doe The Fourth”, 20} field, ok := reflect。TypeOf(user)。Elem()。FieldByName(“name”) if !ok { panic(“Field not found”) } fmt。Println(getStructTag(field))}func getStructTag(f reflect。StructField) string { return string(f。Tag)}
Go 中解析的 tag 是透過反射實現的,反射是指計算機程式在執行時(Run time)可以訪問、檢測和修改它本身狀態或行為的一種能力或動態知道給定資料物件的型別和結構,並有機會修改它。反射將介面變數轉換成反射物件 Type 和 Value;反射可以透過反射物件 Value 還原成原先的介面變數;反射可以用來修改一個變數的值,前提是這個值可以被修改;tag是啥:結構體支援標記,name string `json:name-field` 就是 `json:name-field` 這部分
gorm json yaml gRPC protobuf gin.Bind()都是透過反射來實現的
8、呼叫函式傳入結構體時,應該傳值還是指標? (Golang 都是傳值)
Go 的函式引數傳遞都是值傳遞。所謂值傳遞:指在呼叫函式時將實際引數複製一份傳遞到函式中,這樣在函式中如果對引數進行修改,將不會影響到實際引數。引數傳遞還有引用傳遞,所謂引用傳遞是指在呼叫函式時將實際引數的地址傳遞到函式中,那麼在函式中對引數所進行的修改,將影響到實際引數
因為 Go 裡面的 map,slice,chan 是引用型別。變數區分值型別和引用型別。所謂值型別:變數和變數的值存在同一個位置。所謂引用型別:變數和變數的值是不同的位置,變數的值儲存的是對值的引用。但並不是 map,slice,chan 的所有的變數在函式內都能被修改,不同資料型別的底層儲存結構和實現可能不太一樣,情況也就不一樣。
9、講講 Go 的 slice 底層資料結構和一些特性?
答:Go 的 slice 底層資料結構是由一個 array 指標指向底層陣列,len 表示切片長度,cap 表示切片容量。slice 的主要實現是擴容。對於 append 向 slice 新增元素時,假如 slice 容量夠用,則追加新元素進去,slice。len++,返回原來的 slice。當原容量不夠,則 slice 先擴容,擴容之後 slice 得到新的 slice,將元素追加進新的 slice,slice。len++,返回新的 slice。對於切片的擴容規則:當切片比較小時(容量小於 1024),則採用較大的擴容倍速進行擴容(新的擴容會是原來的 2 倍),避免頻繁擴容,從而減少記憶體分配的次數和資料複製的代價。當切片較大的時(原來的 slice 的容量大於或者等於 1024),採用較小的擴容倍速(新的擴容將擴大大於或者等於原來 1。25 倍),主要避免空間浪費,網上其實很多總結的是 1。25 倍,那是在不考慮記憶體對齊的情況下,實際上還要考慮記憶體對齊,擴容是大於或者等於 1。25 倍。
(關於剛才問的 slice 為什麼傳到函式內可能被修改,如果 slice 在函式內沒有出現擴容,函式外和函式內 slice 變數指向是同一個陣列,則函式內複製的 slice 變數值出現更改,函式外這個 slice 變數值也會被修改。如果 slice 在函式內出現擴容,則函式內變數的值會新生成一個數組(也就是新的 slice,而函式外的 slice 指向的還是原來的 slice,則函式內的修改不會影響函式外的 slice。)
10、講講 Go 的 select 底層資料結構和一些特性?(難點,沒有專案經常可能說不清,面試一般會問你專案中怎麼使用select)
答:go 的 select 為 golang 提供了多路 IO 複用機制,和其他 IO 複用一樣,用於檢測是否有讀寫事件是否 ready。linux 的系統 IO 模型有 select,poll,epoll,go 的 select 和 linux 系統 select 非常相似。
select 結構組成主要是由 case 語句和執行的函式組成 select 實現的多路複用是:每個執行緒或者程序都先到註冊和接受的 channel(裝置)註冊,然後阻塞,然後只有一個執行緒在運輸,當註冊的執行緒和程序準備好資料後,裝置會根據註冊的資訊得到相應的資料。
select 的特性
1)select 操作至少要有一個 case 語句,出現讀寫 nil 的 channel 該分支會忽略,在 nil 的 channel 上操作則會報錯。
2)select 僅支援管道,而且是單協程操作。
3)每個 case 語句僅能處理一個管道,要麼讀要麼寫。
4)多個 case 語句的執行順序是隨機的。
5)存在 default 語句,select 將不會阻塞,但是存在 default 會影響效能。
11、講講 Go 的 defer 底層資料結構和一些特性?
答:每個 defer 語句都對應一個_defer 例項,多個例項使用指標連線起來形成一個單連表,儲存在 gotoutine 資料結構中,每次插入_defer 例項,均插入到連結串列的頭部,函式結束再一次從頭部取出,從而形成後進先出的效果。
defer 的規則總結
:
延遲函式的引數是 defer 語句出現的時候就已經確定了的。
延遲函式執行按照後進先出的順序執行,即先出現的 defer 最後執行。
延遲函式可能操作主函式的返回值。
申請資源後立即使用 defer 關閉資源是個好習慣。
12、單引號,雙引號,反引號的區別?
單引號,表示byte型別或rune型別,對應 uint8和int32型別,預設是 rune 型別。byte用來強調資料是raw data,而不是數字;而rune用來表示Unicode的code point。
雙引號,才是字串,實際上是字元陣列。可以用索引號訪問某位元組,也可以用len()函式來獲取字串所佔的位元組長度。
反引號,表示字串字面量,但不支援任何轉義序列。字面量 raw literal string 的意思是,你定義時寫的啥樣,它就啥樣,你有換行,它就換行。你寫跳脫字元,它也就展示跳脫字元。
二、map相關
1、map 使用注意的點,是否併發安全?
map的型別是map[key],key型別的ke必須是可比較的,通常情況,會選擇內建的基本型別,比如整數、字串做key的型別。如果要使用struct作為key,要保證struct物件在邏輯上是不可變的。在Go語言中,map[key]函式返回結果可以是一個值,也可以是兩個值。map是無序的,如果我們想要保證遍歷map時元素有序,可以使用輔助的資料結構,例如orderedmap。
第一,
一定要先初始化,否則panic
第二,
map型別是容易發生併發訪問問題的。不注意就容易發生程式執行時併發讀寫導致的panic。 Go語言內建的map物件不是執行緒安全的,併發讀寫的時候執行時會有檢查,遇到併發問題就會導致panic。
2、map 迴圈是有序的還是無序的?
無序的, map 因擴張⽽重新雜湊時,各鍵值項儲存位置都可能會發生改變,順序自然也沒法保證了,所以官方避免大家依賴順序,直接打亂處理。就是 for range map 在開始處理迴圈邏輯的時候,就做了隨機播種
3、 map 中刪除一個 key,它的記憶體會釋放麼?(常問)
如果刪除的元素是值型別,如int,float,bool,string以及陣列和struct,map的記憶體不會自動釋放
如果刪除的元素是引用型別,如指標,slice,map,chan等,map的記憶體會自動釋放,但釋放的記憶體是子元素應用型別的記憶體佔用
將map設定為nil後,記憶體被回收。
這個問題還需要大家去搜索下答案,我記得有不一樣的說法,謹慎採用本題答案。
4、怎麼處理對 map 進行併發訪問?有沒有其他方案? 區別是什麼?
方式一、使用內建sync.Map,詳細參考
https://mbd。baidu。com/ma/s/7Hwd9yMc
mbd。baidu。com/ma/s/7Hwd9yMc
方式二、使用讀寫鎖實現併發安全map
https://mbd。baidu。com/ma/s/qO7b0VQU
mbd。baidu。com/ma/s/qO7b0VQU
5、 nil map 和空 map 有何不同?
1)可以對未初始化的map進行取值,但取出來的東西是空:
var m1 map[string]string
fmt。Println(m1[“1”])
////////////////////////////////////////////////////////////////////////////////
2)不能對未初始化的map進行賦值,這樣將會丟擲一個異常:
var m1 map[string]string
m1[“1”] = “1”
panic: assignment to entry in nil map
////////////////////////////////////////////////////////////////////////////////
3) 透過fmt列印map時,空map和nil map結果是一樣的,都為map[]。所以,這個時候別斷定map是空還是nil,而應該透過map == nil來判斷。
nil map 未初始化,空map是長度為空
6、map 的資料結構是什麼?是怎麼實現擴容?
答:golang 中 map 是一個 kv 對集合。底層使用 hash table,用連結串列來解決衝突 ,出現衝突時,不是每一個 key 都申請一個結構透過連結串列串起來,而是以 bmap 為最小粒度掛載,一個 bmap 可以放 8 個 kv。在雜湊函式的選擇上,會在程式啟動時,檢測 cpu 是否支援 aes,如果支援,則使用 aes hash,否則使用 memhash。每個 map 的底層結構是 hmap,是有若干個結構為 bmap 的 bucket 組成的陣列。每個 bucket 底層都採用連結串列結構。
hmap 的結構如下:
type hmap struct { count int // 元素個數 flags uint8 B uint8 // 擴容常量相關欄位B是buckets陣列的長度的對數 2^B noverflow uint16 // 溢位的bucket個數 hash0 uint32 // hash seed buckets unsafe。Pointer // buckets 陣列指標 oldbuckets unsafe。Pointer // 結構擴容的時候用於賦值的buckets陣列 nevacuate uintptr // 搬遷進度 extra *mapextra // 用於擴容的指標}
map 的容量大小
底層呼叫 makemap 函式,計算得到合適的 B,map 容量最多可容納 6。52^B 個元素,6。5 為裝載因子閾值常量。裝載因子的計算公式是:裝載因子=填入表中的元素個數/散列表的長度,裝載因子越大,說明空閒位置越少,衝突越多,散列表的效能會下降。底層呼叫 makemap 函式,計算得到合適的 B,map 容量最多可容納 6。52^B 個元素,6。5 為裝載因子閾值常量。裝載因子的計算公式是:裝載因子=填入表中的元素個數/散列表的長度,裝載因子越大,說明空閒位置越少,衝突越多,散列表的效能會下降。
觸發 map 擴容的條件
1)裝載因子超過閾值,原始碼裡定義的閾值是 6。5。
2)overflow 的 bucket 數量過多 map 的 bucket 定位和 key 的定位高八位用於定位 bucket,低八位用於定位 key,快速試錯後再進行完整對比
7、slices能作為map型別的key嗎?
當時被問的一臉懵逼,其實是這個問題的變種:golang 哪些型別可以作為map key?
答案是:
在golang規範中,可比較的型別都可以作為map key;
這個問題又延伸到在:golang規範中,哪些資料型別可以比較?
不能作為map key 的型別包括:
slices
maps
functions
詳細參考:
golang 哪些型別可以作為map key
三、context相關
1、context 結構是什麼樣的?context 使用場景和用途?
(難,也常常問你專案中怎麼用,光靠記答案很難讓面試官滿意,反正有各種結合實際的問題)
參考連結:
go context詳解 - 捲毛狒狒 - 部落格園
www。cnblogs。com/juanmaofeifei/p/14439957。html
答:Go 的 Context 的資料結構包含 Deadline,Done,Err,Value,Deadline 方法返回一個 time。Time,表示當前 Context 應該結束的時間,ok 則表示有結束時間,Done 方法當 Context 被取消或者超時時候返回的一個 close 的 channel,告訴給 context 相關的函式要停止當前工作然後返回了,Err 表示 context 被取消的原因,Value 方法表示 context 實現共享資料儲存的地方,是協程安全的。context 在業務中是經常被使用的,
其主要的應用 :
1:上下文控制,2:多個 goroutine 之間的資料互動等,3:超時控制:到某個時間點超時,過多久超時。
四、channel相關
1、channel 是否執行緒安全?鎖用在什麼地方?
就著圖片裡面的答案看看吧。
2、go channel 的底層實現原理 (資料結構)
底層結構需要描述出來,這個簡單,buf,傳送佇列,接收佇列,lock。
3、nil、關閉的 channel、有資料的 channel,再進行讀、寫、關閉會怎麼樣?(各類變種題型,重要)
還要去了解一下單向channel,如只讀或者只寫通道常見的異常問題,這塊還需要大家自己總結總結,有懂得大佬也可以評論傳送答案。
4、向 channel 傳送資料和從 channel 讀資料的流程是什麼樣的?
傳送流程:
接收流程:
這個沒啥好說的,底層原理,1、2、3描述出來,保證面試官滿意。具體的文字描述下面一題有,channel的概念多且複雜,腦海中有個總分的概念,否則你說的再多,面試官也抓不住你說的重點,等於白說。問題5已經為大家總結好了。
5、講講 Go 的 chan 底層資料結構和主要使用場景
答:channel 的資料結構包含 qccount 當前佇列中剩餘元素個數,dataqsiz 環形佇列長度,即可以存放的元素個數,buf 環形佇列指標,elemsize 每個元素的大小,closed 標識關閉狀態,elemtype 元素型別,sendx 佇列下表,指示元素寫入時存放到佇列中的位置,recv 佇列下表,指示元素從佇列的該位置讀出。recvq 等待讀訊息的 goroutine 佇列,sendq 等待寫訊息的 goroutine 佇列,lock 互斥鎖,chan 不允許併發讀寫。
無緩衝和有緩衝區別:
管道沒有緩衝區,從管道讀資料會阻塞,直到有協程向管道中寫入資料。同樣,向管道寫入資料也會阻塞,直到有協程從管道讀取資料。管道有緩衝區但緩衝區沒有資料,從管道讀取資料也會阻塞,直到協程寫入資料,如果管道滿了,寫資料也會阻塞,直到協程從緩衝區讀取資料。
channel 的一些特點
1)、讀寫值 nil 管道會永久阻塞 2)、關閉的管道讀資料仍然可以讀資料 3)、往關閉的管道寫資料會 panic 4)、關閉為 nil 的管道 panic 5)、關閉已經關閉的管道 panic
向 channel 寫資料的流程:
如果等待接收佇列 recvq 不為空,說明緩衝區中沒有資料或者沒有緩衝區,此時直接從 recvq 取出 G,並把資料寫入,最後把該 G 喚醒,結束髮送過程; 如果緩衝區中有空餘位置,將資料寫入緩衝區,結束髮送過程; 如果緩衝區中沒有空餘位置,將待發送資料寫入 G,將當前 G 加入 sendq,進入睡眠,等待被讀 goroutine 喚醒;
向 channel 讀資料的流程:
如果等待發送佇列 sendq 不為空,且沒有緩衝區,直接從 sendq 中取出 G,把 G 中資料讀出,最後把 G 喚醒,結束讀取過程; 如果等待發送佇列 sendq 不為空,此時說明緩衝區已滿,從緩衝區中首部讀出資料,把 G 中資料寫入緩衝區尾部,把 G 喚醒,結束讀取過程; 如果緩衝區中有資料,則從緩衝區取出資料,結束讀取過程;將當前 goroutine 加入 recvq,進入睡眠,等待被寫 goroutine 喚醒;
使用場景:
訊息傳遞、訊息過濾,訊號廣播,事件訂閱與廣播,請求、響應轉發,任務分發,結果彙總,併發控制,限流,同步與非同步
五、GMP相關
1、什麼是 GMP?(必問)
答:G 代表著 goroutine,P 代表著上下文處理器,M 代表 thread 執行緒,在 GPM 模型,有一個全域性佇列(Global Queue):存放等待執行的 G,還有一個 P 的本地佇列:也是存放等待執行的 G,但數量有限,不超過 256 個。GPM 的排程流程從 go func()開始建立一個 goroutine,新建的 goroutine 優先儲存在 P 的本地佇列中,如果 P 的本地佇列已經滿了,則會儲存到全域性佇列中。M 會從 P 的佇列中取一個可執行狀態的 G 來執行,如果 P 的本地佇列為空,就會從其他的 MP 組合偷取一個可執行的 G 來執行,當 M 執行某一個 G 時候發生系統呼叫或者阻塞,M 阻塞,如果這個時候 G 在執行,runtime 會把這個執行緒 M 從 P 中摘除,然後建立一個新的作業系統執行緒來服務於這個 P,當 M 系統呼叫結束時,這個 G 會嘗試獲取一個空閒的 P 來執行,並放入到這個 P 的本地佇列,如果這個執行緒 M 變成休眠狀態,加入到空閒執行緒中,然後整個 G 就會被放入到全域性佇列中。
關於 G,P,M 的個數問題,G 的個數理論上是無限制的,但是受記憶體限制,P 的數量一般建議是邏輯 CPU 數量的 2 倍,M 的資料預設啟動的時候是 10000,核心很難支援這麼多執行緒數,所以整個限制客戶忽略,M 一般不做設定,設定好 P,M 一般都是要大於 P。
2、程序、執行緒、協程有什麼區別?(必問)
程序:是應用程式的啟動例項,每個程序都有獨立的記憶體空間,不同的程序透過程序間的通訊方式來通訊。
執行緒:從屬於程序,每個程序至少包含一個執行緒,執行緒是 CPU 排程的基本單位,多個執行緒之間可以共享程序的資源並透過共享記憶體等執行緒間的通訊方式來通訊。
協程:為輕量級執行緒,與執行緒相比,協程不受作業系統的排程,協程的排程器由使用者應用程式提供,協程排程器按照排程策略把協程排程到執行緒中執行
3、搶佔式排程是如何搶佔的?
基於協作式搶佔
基於訊號量搶佔
就像作業系統要負責執行緒的排程一樣,Go的runtime要負責goroutine的排程。現代作業系統排程執行緒都是搶佔式的,我們不能依賴使用者程式碼主動讓出CPU,或者因為IO、鎖等待而讓出,這樣會造成排程的不公平。基於經典的時間片演算法,當執行緒的時間片用完之後,會被時鐘中斷給打斷,排程器會將當前執行緒的執行上下文進行儲存,然後恢復下一個執行緒的上下文,分配新的時間片令其開始執行。這種搶佔對於執行緒本身是無感知的,系統底層支援,不需要開發人員特殊處理。
基於時間片的搶佔式排程有個明顯的優點,能夠避免CPU資源持續被少數執行緒佔用,從而使其他執行緒長時間處於飢餓狀態。goroutine的排程器也用到了時間片演算法,但是和作業系統的執行緒排程還是有些區別的,因為整個Go程式都是執行在使用者態的,所以不能像作業系統那樣利用時鐘中斷來打斷執行中的goroutine。也得益於完全在使用者態實現,goroutine的排程切換更加輕量。
上面這兩段文字只是對排程的一個概括,具體的協作式排程、訊號量排程大家還需要去詳細瞭解,這偏底層了,大廠或者中高階開發會問。(位元組就問了)
4、M 和 P 的數量問題?
p預設cpu核心數
M與P的數量沒有絕對關係,一個M阻塞,P就會去建立或者切換另一個M,所以,即使P的預設數量是1,也有可能會建立很多個M出來
【Go語言排程模型G、M、P的數量多少合適?】
https://www。kancloud。cn/aceld/golang/1958305
GMP數量這一塊,結論很好記,沒用專案經驗的話,問了專案中怎麼用可能容易卡殼。
六、鎖相關
1、除了 mutex 以外還有那些方式安全讀寫共享變數?
* 將共享變數的讀寫放到一個 goroutine 中,其它 goroutine 透過 channel 進行讀寫操作。
* 可以用個數為 1 的訊號量(semaphore)實現互斥
* 透過 Mutex 鎖實現
2、Go 如何實現原子操作?
答:原子操作就是不可中斷的操作,外界是看不到原子操作的中間狀態,要麼看到原子操作已經完成,要麼看到原子操作已經結束。在某個值的原子操作執行的過程中,CPU 絕對不會再去執行其他針對該值的操作,那麼其他操作也是原子操作。
Go 語言的標準庫程式碼包 sync/atomic 提供了原子的讀取(Load 為字首的函式)或寫入(Store 為字首的函式)某個值(這裡細節還要多去查查資料)。
原子操作與互斥鎖的區別
1)、互斥鎖是一種資料結構,用來讓一個執行緒執行程式的關鍵部分,完成互斥的多個操作。
2)、原子操作是針對某個值的單個互斥操作。
3、Mutex 是悲觀鎖還是樂觀鎖?悲觀鎖、樂觀鎖是什麼?
悲觀鎖
悲觀鎖:當要對資料庫中的一條資料進行修改的時候,為了避免同時被其他人修改,最好的辦法就是直接對該資料進行加鎖以防止併發。這種藉助資料庫鎖機制,在修改資料之前先鎖定,再修改的方式被稱之為悲觀併發控制【Pessimistic Concurrency Control,縮寫“PCC”,又名“悲觀鎖”】。
樂觀鎖
樂觀鎖是相對悲觀鎖而言的,樂觀鎖假設資料一般情況不會造成衝突,所以在資料進行提交更新的時候,才會正式對資料的衝突與否進行檢測,如果衝突,則返回給使用者異常資訊,讓使用者決定如何去做。樂觀鎖適用於讀多寫少的場景,這樣可以提高程式的吞吐量
4、Mutex 有幾種模式?
1)正常模式
當前的mutex只有一個goruntine來獲取,那麼沒有競爭,直接返回。
新的goruntine進來,如果當前mutex已經被獲取了,則該goruntine進入一個先入先出的waiter佇列,在mutex被釋放後,waiter按照先進先出的方式獲取鎖。該goruntine會處於自旋狀態(不掛起,繼續佔有cpu)。
新的goruntine進來,mutex處於空閒狀態,將參與競爭。新來的 goroutine 有先天的優勢,它們正在 CPU 中執行,可能它們的數量還不少,所以,在高併發情況下,被喚醒的 waiter 可能比較悲劇地獲取不到鎖,這時,它會被插入到佇列的前面。如果 waiter 獲取不到鎖的時間超過閾值 1 毫秒,那麼,這個 Mutex 就進入到了飢餓模式。
2)飢餓模式
在飢餓模式下,Mutex 的擁有者將直接把鎖交給佇列最前面的 waiter。新來的 goroutine 不會嘗試獲取鎖,即使看起來鎖沒有被持有,它也不會去搶,也不會 spin(自旋),它會乖乖地加入到等待佇列的尾部。 如果擁有 Mutex 的 waiter 發現下面兩種情況的其中之一,它就會把這個 Mutex 轉換成正常模式:
此 waiter 已經是佇列中的最後一個 waiter 了,沒有其它的等待鎖的 goroutine 了;
此 waiter 的等待時間小於 1 毫秒。
5、goroutine 的自旋佔用資源如何解決
自旋鎖是指當一個執行緒在獲取鎖的時候,如果鎖已經被其他執行緒獲取,那麼該執行緒將迴圈等待,然後不斷地判斷是否能夠被成功獲取,直到獲取到鎖才會退出迴圈。
自旋的條件如下:
1)還沒自旋超過 4 次,
2)多核處理器,
3)GOMAXPROCS > 1,
4)p 上本地 goroutine 佇列為空。
mutex 會讓當前的 goroutine 去空轉 CPU,在空轉完後再次呼叫 CAS 方法去嘗試性的佔有鎖資源,直到不滿足自旋條件,則最終會加入到等待佇列裡。
七、併發相關
1、怎麼控制併發數?
第一,有緩衝通道
根據通道中沒有資料時讀取操作陷入阻塞和通道已滿時繼續寫入操作陷入阻塞的特性,正好實現控制併發數量。
func main() { count := 10 // 最大支援併發 sum := 100 // 任務總數 wg := sync。WaitGroup{} //控制主協程等待所有子協程執行完之後再退出。 c := make(chan struct{}, count) // 控制任務併發的chan defer close(c) for i:=0; i 第二,三方庫實現的協程池 panjf2000/ants(比較火) Jeffail/tunny import ( “log” “time” “github。com/Jeffail/tunny”)func main() { pool := tunny。NewFunc(10, func(i interface{}) interface{} { log。Println(i) time。Sleep(time。Second) return nil }) defer pool。Close() for i := 0; i < 500; i++ { go pool。Process(i) } time。Sleep(time。Second * 4)} 2、多個 goroutine 對同一個 map 寫會 panic,異常是否可以用 defer 捕獲? 可以捕獲異常,但是隻能捕獲一次,Go語言,可以使用多值返回來返回錯誤。不要用異常代替錯誤,更不要用來控制流程。在極個別的情況下,才使用Go中引入的Exception處理:defer, panic, recover Go中,對異常處理的原則是:多用error包,少用panic defer func() { if err := recover(); err != nil { // 列印異常,關閉資源,退出此函式 fmt。Println(err) } }() 3、如何優雅的實現一個 goroutine 池 (百度、手寫程式碼,本人面傳音控股被問道:請求數大於消費能力怎麼設計協程池) 這一塊能啃下來,offer滿天飛,這應該是保證高併發系統穩定性、高可用的核心部分之一。 建議參考: Golang學習篇——協程池_Word哥的部落格-CSDN部落格_golang協程池 https://blog。csdn。net/finghting321/article/details/106492915/ 這篇文章的目錄是: 1。 為什麼需要協程池? 2。 簡單的協程池 3。 go-playground/pool 4。 ants(推薦) 所以直接研究ants底層吧,省的造輪子。 八、GC相關 1、go gc 是怎麼實現的?(必問) 答: 細分常見的三個問題:1、GC機制隨著golang版本變化如何變化的?2、三色標記法的流程?3、插入屏障、刪除屏障,混合寫屏障(具體的實現比較難描述,但你要知道屏障的作用:避免程式執行過程中,變數被誤回收;減少STW的時間)4、蝦皮還問了個開放性的題目:你覺得以後GC機制會怎麼最佳化? Go 的 GC 回收有三次演進過程,Go V1。3 之前普通標記清除(mark and sweep)方法,整體過程需要啟動 STW,效率極低。GoV1。5 三色標記法,堆空間啟動寫屏障,棧空間不啟動,全部掃描之後,需要重新掃描一次棧(需要 STW),效率普通。GoV1。8 三色標記法,混合寫屏障機制:棧空間不啟動(全部標記成黑色),堆空間啟用寫屏障,整個過程不要 STW,效率高。 Go1。3 之前的版本所謂標記清除是先啟動 STW 暫停,然後執行標記,再執行資料回收,最後停止 STW。Go1。3 版本標記清除做了點最佳化,流程是:先啟動 STW 暫停,然後執行標記,停止 STW,最後再執行資料回收。 Go1。5 三色標記主要是插入屏障和刪除屏障,寫入屏障的流程:程式開始,全部標記為白色,1)所有的物件放到白色集合,2)遍歷一次根節點,得到灰色節點,3)遍歷灰色節點,將可達的物件,從白色標記灰色,遍歷之後的灰色標記成黑色,4)由於併發特性,此刻外界向在堆中的物件發生新增物件,以及在棧中的物件新增物件,在堆中的物件會觸發插入屏障機制,棧中的物件不觸發,5)由於堆中物件插入屏障,則會把堆中黑色物件新增的白色物件改成灰色,棧中的黑色物件新增的白色物件依然是白色,6)迴圈第 5 步,直到沒有灰色節點,7)在準備回收白色前,重新遍歷掃描一次棧空間,加上 STW 暫停保護棧,防止外界干擾(有新的白色會被新增成黑色)在 STW 中,將棧中的物件一次三色標記,直到沒有灰色,8)停止 STW,清除白色。至於刪除寫屏障,則是遍歷灰色節點的時候出現可達的節點被刪除,這個時候觸發刪除寫屏障,這個可達的被刪除的節點也是灰色,等迴圈三色標記之後,直到沒有灰色節點,然後清理白色,刪除寫屏障會造成一個物件即使被刪除了最後一個指向它的指標也依舊可以活過這一輪,在下一輪 GC 中被清理掉。 GoV1。8 混合寫屏障規則是: 1)GC 開始將棧上的物件全部掃描並標記為黑色(之後不再進行第二次重複掃描,無需 STW),2)GC 期間,任何在棧上建立的新物件,均為黑色。3)被刪除的物件標記為灰色。4)被新增的物件標記為灰色。 2、go 是 gc 演算法是怎麼實現的? (得物,出現頻率低) func GC() { n := atomic。Load(&work。cycles) gcWaitOnMark(n) gcStart(gcTrigger{kind: gcTriggerCycle, n: n + 1}) gcWaitOnMark(n + 1) for atomic。Load(&work。cycles) == n+1 && sweepone() != ^uintptr(0) { sweep。nbgsweep++ Gosched() } for atomic。Load(&work。cycles) == n+1 && atomic。Load(&mheap_。sweepers) != 0 { Gosched() } mp := acquirem() cycle := atomic。Load(&work。cycles) if cycle == n+1 || (gcphase == _GCmark && cycle == n+2) { mProf_PostSweep() } releasem(mp)} 底層原理了,可能大廠,中高階才會問,參考: Golang GC演算法解讀_suchy_sz的部落格-CSDN部落格_go的gc演算法 https://blog。csdn。net/shudaqi2010/article/details/90025192 3、GC 中 stw 時機,各個階段是如何解決的? (百度) 底層原理,自行百度一下,我等渣渣簡歷都過不了BAT,位元組,蝦皮,特使拉以及一些國Q還能收到面試邀約 。 1)在開始新的一輪 GC 週期前,需要呼叫 gcWaitOnMark 方法上一輪 GC 的標記結束(含掃描終止、標記、或標記終止等)。 2)開始新的一輪 GC 週期,呼叫 gcStart 方法觸發 GC 行為,開始掃描標記階段。 3)需要呼叫 gcWaitOnMark 方法等待,直到當前 GC 週期的掃描、標記、標記終止完成。 4)需要呼叫 sweepone 方法,掃描未掃除的堆跨度,並持續掃除,保證清理完成。在等待掃除完畢前的阻塞時間,會呼叫 Gosched 讓出。 5)在本輪 GC 已經基本完成後,會呼叫 mProf_PostSweep 方法。以此記錄最後一次標記終止時的堆配置檔案快照。 6)結束,釋放 M。 4、GC 的觸發時機? 初級必問,分為系統觸發和主動觸發。 1)gcTriggerHeap:當所分配的堆大小達到閾值(由控制器計算的觸發堆的大小)時,將會觸發。 2)gcTriggerTime:當距離上一個 GC 週期的時間超過一定時間時,將會觸發。時間週期以runtime。forcegcperiod 變數為準,預設 2 分鐘。 3)gcTriggerCycle:如果沒有開啟 GC,則啟動 GC。 4)手動觸發的 runtime。GC 方法。 九、記憶體相關 1、談談記憶體洩露,什麼情況下記憶體會洩露?怎麼定位排查記憶體洩漏問題? 答:go 中的記憶體洩漏一般都是 goroutine 洩漏,就是 goroutine 沒有被關閉,或者沒有新增超時控制,讓 goroutine 一隻處於阻塞狀態,不能被 GC。 記憶體洩露有下面一些情況 1)如果 goroutine 在執行時被阻塞而無法退出,就會導致 goroutine 的記憶體洩漏,一個 goroutine 的最低棧大小為 2KB,在高併發的場景下,對記憶體的消耗也是非常恐怖的。 2)互斥鎖未釋放或者造成死鎖會造成記憶體洩漏 3)time。Ticker 是每隔指定的時間就會向通道內寫資料。作為迴圈觸發器,必須呼叫 stop 方法才會停止,從而被 GC 掉,否則會一直佔用記憶體空間。 4)字串的擷取引發臨時性的記憶體洩漏 func main() { var str0 = “12345678901234567890” str1 := str0[:10]} 5)切片擷取引起子切片記憶體洩漏 func main() { var s0 = []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} s1 := s0[:3]} 6)函式陣列傳參引發記憶體洩漏【如果我們在函式傳參的時候用到了陣列傳參,且這個陣列夠大(我們假設陣列大小為 100 萬,64 位機上消耗的記憶體約為 800w 位元組,即 8MB 記憶體),或者該函式短時間內被呼叫 N 次,那麼可想而知,會消耗大量記憶體,對效能產生極大的影響,如果短時間內分配大量記憶體,而又來不及 GC,那麼就會產生臨時性的記憶體洩漏,對於高併發場景相當可怕。】 排查方式: 一般透過 pprof 是 Go 的效能分析工具,在程式執行過程中,可以記錄程式的執行資訊,可以是 CPU 使用情況、記憶體使用情況、goroutine 執行情況等,當需要效能調優或者定位 Bug 時候,這些記錄的資訊是相當重要。 當然你能說說具體的分析指標更加分咯,有的面試官就喜歡他問什麼,你簡潔的回答什麼,不喜歡巴拉巴拉詳細解釋一通,比如蝦P面試官,不過他考察的內容特別多,可能是為了節約時間。 2、知道 golang 的記憶體逃逸嗎?什麼情況下會發生記憶體逃逸?(必問) 答:1)本該分配到棧上的變數,跑到了堆上,這就導致了記憶體逃逸。2)棧是高地址到低地址,棧上的變數,函式結束後變數會跟著回收掉,不會有額外效能的開銷。3)變數從棧逃逸到堆上,如果要回收掉,需要進行 gc,那麼 gc 一定會帶來額外的效能開銷。程式語言不斷最佳化 gc 演算法,主要目的都是為了減少 gc 帶來的額外效能開銷,變數一旦逃逸會導致效能開銷變大。 記憶體逃逸的情況如下: 1)方法內返回區域性變數指標。 2)向 channel 傳送指標資料。 3)在閉包中引用包外的值。 4)在 slice 或 map 中儲存指標。 5)切片(擴容後)長度太大。 6)在 interface 型別上呼叫方法。 3、請簡述 Go 是如何分配記憶體的? mcache mcentral mheap mspan Go 程式啟動的時候申請一大塊記憶體,並且劃分 spans,bitmap,areana 區域;arena 區域按照頁劃分成一個個小塊,span 管理一個或者多個頁,mcentral 管理多個 span 供現場申請使用;mcache 作為執行緒私有資源,來源於 mcentral。 這裡描述的比較簡單,你可以自己再去搜索下更簡潔完整的答案。 4、Channel 分配在棧上還是堆上?哪些物件分配在堆上,哪些物件分配在棧上? Channel 被設計用來實現協程間通訊的元件,其作用域和生命週期不可能僅限於某個函式內部,所以 golang 直接將其分配在堆上 準確地說,你並不需要知道。Golang 中的變數只要被引用就一直會存活,儲存在堆上還是棧上由內部實現決定而和具體的語法沒有關係。 知道變數的儲存位置確實和效率程式設計有關係。如果可能,Golang 編譯器會將函式的區域性變數分配到函式棧幀(stack frame)上。然而,如果編譯器不能確保變數在函式 return 之後不再被引用,編譯器就會將變數分配到堆上。而且,如果一個區域性變數非常大,那麼它也應該被分配到堆上而不是棧上。 當前情況下,如果一個變數被取地址,那麼它就有可能被分配到堆上,然而,還要對這些變數做逃逸分析,如果函式 return 之後,變數不再被引用,則將其分配到棧上。 5、介紹一下大物件小物件,為什麼小物件多了會造成 gc 壓力? 小於等於 32k 的物件就是小物件,其它都是大物件。一般小物件透過 mspan 分配記憶體;大物件則直接由 mheap 分配記憶體。通常小物件過多會導致 GC 三色法消耗過多的 CPU。最佳化思路是,減少物件分配。 小物件:如果申請小物件時,發現當前記憶體空間不存在空閒跨度時,將會需要呼叫 nextFree 方法獲取新的可用的物件,可能會觸發 GC 行為。 大物件:如果申請大於 32k 以上的大物件時,可能會觸發 GC 行為。 十、其他問題 1、Go 多返回值怎麼實現的? 答:Go 傳參和返回值是透過 FP+offset 實現,並且儲存在呼叫函式的棧幀中。FP 棧底暫存器,指向一個函式棧的頂部;PC 程式計數器,指向下一條執行指令;SB 指向靜態資料的基指標,全域性符號;SP 棧頂暫存器。 2、講講 Go 中主協程如何等待其餘協程退出? 答:Go 的 sync。WaitGroup 是等待一組協程結束,sync。WaitGroup 只有 3 個方法,Add()是新增計數,Done()減去一個計數,Wait()阻塞直到所有的任務完成。Go 裡面還能透過有緩衝的 channel 實現其阻塞等待一組協程結束,這個不能保證一組 goroutine 按照順序執行,可以併發執行協程。Go 裡面能透過無緩衝的 channel 實現其阻塞等待一組協程結束,這個能保證一組 goroutine 按照順序執行,但是不能併發執行。 囉嗦一句: 迴圈智慧二面,手寫程式碼部分時,三個協程按交替順序列印數字,最後題目做出來了,問我程式碼中Add()是什麼意思,我回答的不是很清晰,這家公司就沒有然後了。Add()表示協程計數,可以一次Add多個,如Add(3),可以多次Add(1);然後每個子協程必須呼叫done(),這樣才能保證所有子協程結束,主協程才能結束。 3、Go 語言中不同的型別如何比較是否相等? 答:像 string,int,float interface 等可以透過 reflect。DeepEqual 和等於號進行比較,像 slice,struct,map 則一般使用 reflect。DeepEqual 來檢測是否相等。 4、Go 中 init 函式的特徵? 答:一個包下可以有多個 init 函式,每個檔案也可以有多個 init 函式。多個 init 函式按照它們的檔名順序逐個初始化。應用初始化時初始化工作的順序是,從被匯入的最深層包開始進行初始化,層層遞出最後到 main 包。不管包被匯入多少次,包內的 init 函式只會執行一次。應用初始化時初始化工作的順序是,從被匯入的最深層包開始進行初始化,層層遞出最後到 main 包。但包級別變數的初始化先於包內 init 函式的執行。 5、Go 中 uintptr 和 unsafe。Pointer 的區別? 答:unsafe。Pointer 是通用指標型別,它不能參與計算,任何型別的指標都可以轉化成 unsafe。Pointer,unsafe。Pointer 可以轉化成任何型別的指標,uintptr 可以轉換為 unsafe。Pointer,unsafe。Pointer 可以轉換為 uintptr。uintptr 是指標運算的工具,但是它不能持有指標物件(意思就是它跟指標物件不能互相轉換),unsafe。Pointer 是指標物件進行運算(也就是 uintptr)的橋樑。 6、golang共享記憶體(互斥鎖)方法實現傳送多個get請求 待補充, 能看到這裡還能給出答案的人,有嗎???? 7、從陣列中取一個相同大小的slice有成本嗎? 或者這麼問:從切片中取一個相同大小的陣列有成本嗎? 這是愛立信的二面題目,這個問題我至今還沒搞懂,不知道從什麼切入點去分析,歡迎指教。 PS:愛立信面試都要英文自我介紹,以及問答,如果英文回答不上來,會直接切換成中文。 8、PHP能實現併發處理事務嗎? 多程序:pcntl擴充套件 php pcntl用法-PHP問題-PHP中文網 www。php。cn/php-ask-473095。html 多執行緒: 1)swoole(常用,生態完善) 2)pthread擴充套件(不常用) 為什麼php多執行緒沒人用? 23 贊同 · 18 評論回答 參考並致謝 1、可可醬 可可醬:Golang常見面試題 2、Bel_Ami同學 golang 面試題(從基礎到高階)