您好,登錄后才能下訂單哦!
這篇文章主要介紹“GO面試題及答案有哪些”的相關知識,小編通過實際案例向大家展示操作過程,操作方法簡單快捷,實用性強,希望這篇“GO面試題及答案有哪些”文章能幫助大家解決問題。
GO1.17版本及之前
當新切片需要的容量cap大于兩倍擴容的容量,則直接按照新切片需要的容量擴容;
當原 slice 容量 < 1024 的時候,新 slice 容量變成原來的 2 倍;
當原 slice 容量 > 1024,進入一個循環,每次容量變成原來的1.25倍,直到大于期望容量。
GO1.18之后
當新切片需要的容量cap大于兩倍擴容的容量,則直接按照新切片需要的容量擴容;
當原 slice 容量 < threshold 的時候,新 slice 容量變成原來的 2 倍;
當原 slice 容量 > threshold,進入一個循環,每次容量增加(舊容量+3*threshold)/4。
slice底層結構并沒有使用加鎖的方式,不支持并發讀寫
map 是一個指針 占用8個字節(64位計算機),指向hmap結構體,hmap包含多個bmap數組(桶)
type hmap struct {
count int //元素個數,調用len(map)時直接返回
flags uint8 //標志map當前狀態,正在刪除元素、添加元素.....
B uint8 //單元(buckets)的對數 B=5表示能容納32個元素 B隨著map容量增大而變大
noverflow uint16 //單元(buckets)溢出數量,如果一個單元能存8個key,此時存儲了9個,溢出了,就需要再增加一個單元
hash0 uint32 //哈希種子
buckets unsafe.Pointer //指向單元(buckets)數組,大小為2^B,可以為nil
oldbuckets unsafe.Pointer //擴容的時候,buckets長度會是oldbuckets的兩倍
nevacute uintptr //指示擴容進度,小于此buckets遷移完成
extra *mapextra //與gc相關 可選字段
}
type bmap struct {
tophash [bucketCnt]uint8
}
//實際上編譯期間會生成一個新的數據結構
type bmap struct {
topbits [8]uint8 //key hash值前8位 用于快速定位keys的位置
keys [8]keytype //鍵
values [8]valuetype //值
pad uintptr
overflow uintptr //指向溢出桶 無符號整形 優化GC
}
擴容時機:向 map 插入新 key 的時候,會進行條件檢測,符合下面這 2 個條件,就會觸發擴容
擴容條件:
1.超過負載 map元素個數 > 6.5(負載因子) * 桶個數
2.溢出桶太多
當桶總數<2^15時,如果溢出桶總數>=桶總數,則認為溢出桶過多
當桶總數>2^15時,如果溢出桶總數>=2^15,則認為溢出桶過多
擴容機制:
雙倍擴容:針對條件1,新建一個buckets數組,新的buckets大小是原來的2倍,然后舊buckets數據搬遷到新的buckets。
等量擴容:針對條件2,并不擴大容量,buckets數量維持不變,重新做一遍類似雙倍擴容的搬遷動作,把松散的鍵值對重新排列一次,使得同一個 bucket 中的 key 排列地更緊密,節省空間,提高 bucket 利用率,進而保證更快的存取。
漸進式擴容:
插入修改刪除key的時候,都會嘗試進行搬遷桶的工作,每次都會檢查oldbucket是否nil,如果不是nil則每次搬遷2個桶,螞蟻搬家一樣漸進式擴容
map每次遍歷,都會從一個隨機值序號的桶,再從其中隨機的cell開始遍歷,并且擴容后,原來桶中的key會落到其他桶中,本身就會造成失序
如果想順序遍歷map,先把key放到切片排序,再按照key的順序遍歷map
var sl []int
for k := range m {
sl = append(sl, k)
}
sort.Ints(sl)
for _,k:= range sl {
fmt.Print(m[k])
}
map設計就不是用來多個協程高并發訪問的
多個協程同時對map進行并發讀寫,程序會panic
如果想線程安全,可以使用sync.RWLock 鎖
sync.map
這個包里面的map實現了鎖,是線程安全的
1.寫保護機制
先插hmap的標志位flags,如果flags寫標志位此時是1,說明其他協程正在寫操作,直接panic
2.計算hash值
key經過哈希函數計算后,得到64bit(64位CPU)
10010111 | 101011101010110101010101101010101010 | 10010
3.找到hash對應的桶
上面64位后5(hmap的B值)位定位所存放的桶
如果當前正在擴容中,并且定位到舊桶數據還未完成遷移,則使用舊的桶
4.遍歷桶查找
上面64位前8位用來在tophash數組查找快速判斷key是否在當前的桶中,如果不在需要去溢出桶查找
5.返回key對應的指針
GO采用鏈地址法解決沖突,具體就是插入key到map中時,當key定位的桶填滿8個元素后,將會創建一個溢出桶,并且將溢出桶插入當前桶的所在鏈表尾部
負載因子 = 哈希表存儲的元素個數 / 桶個數
Go 官方發現:裝載因子越大,填入的元素越多,空間利用率就越高,但發生哈希沖突的幾率就變大。
裝載因子越小,填入的元素越少,沖突發生的幾率減小,但空間浪費也會變得更多,而且還會提高擴容操作的次數
Go 官方取了一個相對適中的值,把 Go 中的 map 的負載因子硬編碼為 6.5,這就是 6.5 的選擇緣由。
這意味著在 Go 語言中,當 map存儲的元素個數大于或等于 6.5 * 桶個數 時,就會觸發擴容行為。
type Map struct {
mu Mutex
read atomic.Value
dirty map[interface()]*entry
misses int
}
對比原始map:
和原始map+RWLock的實現并發的方式相比,減少了加鎖對性能的影響。它做了一些優化:可以無鎖訪問read map,而且會優先操作read map,倘若只操作read map就可以滿足要求,那就不用去操作write map(dirty),所以在某些特定場景中它發生鎖競爭的頻率會遠遠小于map+RWLock的實現方式
優點:
適合讀多寫少的場景
缺點:
寫多的場景,會導致 read map 緩存失效,需要加鎖,沖突變多,性能急劇下降
通過var聲明或者make函數創建的channel變量是一個存儲在函數棧幀上的指針,占用8個字節,指向堆上的hchan結構體
type hchan struct {
closed uint32 // channel是否關閉的標志
elemtype *_type // channel中的元素類型
// channel分為無緩沖和有緩沖兩種。
// 對于有緩沖的channel存儲數據,使用了 ring buffer(環形緩沖區) 來緩存寫入的數據,本質是循環數組
// 為啥是循環數組?普通數組不行嗎,普通數組容量固定更適合指定的空間,彈出元素時,普通數組需要全部都前移
// 當下標超過數組容量后會回到第一個位置,所以需要有兩個字段記錄當前讀和寫的下標位置
buf unsafe.Pointer // 指向底層循環數組的指針(環形緩沖區)
qcount uint // 循環數組中的元素數量
dataqsiz uint // 循環數組的長度
elemsize uint16 // 元素的大小
sendx uint // 下一次寫下標的位置
recvx uint // 下一次讀下標的位置
// 嘗試讀取channel或向channel寫入數據而被阻塞的goroutine
recvq waitq // 讀等待隊列
sendq waitq // 寫等待隊列
lock mutex //互斥鎖,保證讀寫channel時不存在并發競爭問題
}
等待隊列:
雙向鏈表,包含一個頭結點和一個尾結點
每個節點是一個sudog結構體變量,記錄哪個協程在等待,等待的是哪個channel,等待發送/接收的數據在哪里
type waitq struct {
first *sudog
last *sudog
}
type sudog struct {
g *g
next *sudog
prev *sudog
elem unsafe.Pointer
c *hchan
...
}
創建時:
創建時會做一些檢查:
- 元素大小不能超過 64K
- 元素的對齊大小不能超過 maxAlign 也就是 8 字節
- 計算出來的內存是否超過限制
創建時的策略:
- 如果是無緩沖的 channel,會直接給 hchan 分配內存
- 如果是有緩沖的 channel,并且元素不包含指針,那么會為 hchan 和底層數組分配一段連續的地址
- 如果是有緩沖的 channel,并且元素包含指針,那么會為 hchan 和底層數組分別分配地址
發送時:
- 如果 channel 的讀等待隊列存在接收者goroutine
- 將數據**直接發送**給第一個等待的 goroutine, **喚醒接收的 goroutine**
- 如果 channel 的讀等待隊列不存在接收者goroutine
- 如果循環數組buf未滿,那么將會把數據發送到循環數組buf的隊尾
- 如果循環數組buf已滿,這個時候就會走阻塞發送的流程,將當前 goroutine 加入寫等待隊列,并**掛起等待喚醒**
接收時:
- 如果 channel 的寫等待隊列存在發送者goroutine
- 如果是無緩沖 channel,**直接**從第一個發送者goroutine那里把數據拷貝給接收變量,**喚醒發送的 goroutine**
- 如果是有緩沖 channel(已滿),將循環數組buf的隊首元素拷貝給接收變量,將第一個發送者goroutine的數據拷貝到 buf循環數組隊尾,**喚醒發送的 goroutine**
- 如果 channel 的寫等待隊列不存在發送者goroutine
- 如果循環數組buf非空,將循環數組buf的隊首元素拷貝給接收變量
- 如果循環數組buf為空,這個時候就會走阻塞接收的流程,將當前 goroutine 加入讀等待隊列,并**掛起等待喚醒**
channel有2種類型:無緩沖、有緩沖
channel有3種模式:寫操作模式(單向通道)、讀操作模式(單向通道)、讀寫操作模式(雙向通道)
寫操作模式 make(chan<- int)
讀操作模式 make(<-chan int)
讀寫操作模式 make(chan int)
channel有3種狀態:未初始化、正常、關閉
操作\狀態 | 未初始化 | 關閉 | 正常 |
---|---|---|---|
關閉 | panic | panic | 正常 |
發送 | 永遠阻塞導致死鎖 | panic | 阻塞或者成功發送 |
接收 | 永遠阻塞導致死鎖 | 緩沖區為空則為零值, 否則可以繼續讀 | 阻塞或者成功接收 |
注意點:
一個 channel不能多次關閉,會導致painc
如果多個 goroutine 都監聽同一個 channel,那么 channel 上的數據都可能隨機被某一個 goroutine 取走進行消費
如果多個 goroutine 監聽同一個 channel,如果這個 channel 被關閉,則所有 goroutine 都能收到退出信號
不同協程通過channel進行通信,本身的使用場景就是多線程,為了保證數據的一致性,必須實現線程安全
channel的底層實現中,hchan結構體中采用Mutex鎖來保證數據讀寫安全。在對循環數組buf中的數據進行入隊和出隊操作時,必須先獲取互斥鎖,才能操作channel數據
func deadlock1() { //無緩沖channel只寫不讀
ch := make(chan int)
ch <- 3 // 這里會發生一直阻塞的情況,執行不到下面一句
}
func deadlock2() { //無緩沖channel讀在寫后面
ch := make(chan int)
ch <- 3 // 這里會發生一直阻塞的情況,執行不到下面一句
num := <-ch
fmt.Println("num=", num)
}
func deadlock3() { //無緩沖channel讀在寫后面
ch := make(chan int)
ch <- 100 // 這里會發生一直阻塞的情況,執行不到下面一句
go func() {
num := <-ch
fmt.Println("num=", num)
}()
time.Sleep(time.Second)
}
func deadlock3() { //有緩沖channel寫入超過緩沖區數量
ch := make(chan int, 3)
ch <- 3
ch <- 4
ch <- 5
ch <- 6 // 這里會發生一直阻塞的情況
}
func deadlock4() { //空讀
ch := make(chan int)
// ch := make(chan int, 1)
fmt.Println(<-ch) // 這里會發生一直阻塞的情況
}
func deadlock5() { //互相等對方造成死鎖
ch2 := make(chan int)
ch3 := make(chan int)
go func() {
for {
select {
case num := <-ch2:
fmt.Println("num=", num)
ch3 <- 100
}
}
}()
for {
select {
case num := <-ch3:
fmt.Println("num=", num)
ch2 <- 300
}
}
}
Go sync包提供了兩種鎖類型:互斥鎖sync.Mutex 和 讀寫互斥鎖sync.RWMutex,都屬于悲觀鎖。
鎖的實現一般會依賴于原子操作、信號量,通過atomic 包中的一些原子操作來實現鎖的鎖定,通過信號量來實現線程的阻塞與喚醒
在正常模式下,鎖的等待者會按照先進先出的順序獲取鎖。但是剛被喚起的 Goroutine 與新創建的 Goroutine 競爭時,大概率會獲取不到鎖,在這種情況下,這個被喚醒的 Goroutine 會加入到等待隊列的前面。 如果一個等待的 Goroutine 超過1ms 沒有獲取鎖,那么它將會把鎖轉變為饑餓模式。
Go在1.9中引入優化,目的保證互斥鎖的公平性。在饑餓模式中,互斥鎖會直接交給等待隊列最前面的 Goroutine。新的 Goroutine 在該狀態下不能獲取鎖、也不會進入自旋狀態,它們只會在隊列的末尾等待。如果一個 Goroutine 獲得了互斥鎖并且它在隊列的末尾或者它等待的時間少于 1ms,那么當前的互斥鎖就會切換回正常模式。
線程沒有獲取到鎖時常見有2種處理方式:
- 一種是沒有獲取到鎖的線程就一直循環等待判斷該資源是否已經釋放鎖,這種鎖也叫做自旋鎖,它不用將線程阻塞起來, 適用于并發低且程序執行時間短的場景,缺點是cpu占用較高
- 另外一種處理方式就是把自己阻塞起來,會釋放CPU給其他線程,內核會將線程置為「睡眠」狀態,等到鎖被釋放后,內核會在合適的時機喚醒該線程,適用于高并發場景,缺點是有線程上下文切換的開銷
Go語言中的Mutex實現了自旋與阻塞兩種場景,當滿足不了自旋條件時,就會進入阻塞
**允許自旋的條件:**
1. 鎖已被占用,并且鎖不處于饑餓模式。
2. 積累的自旋次數小于最大自旋次數(active_spin=4)。
3. cpu 核數大于 1。
4. 有空閑的 P。
5. 當前 goroutine 所掛載的 P 下,本地待運行隊列為空。
讀寫鎖的底層是基于互斥鎖實現的。
寫鎖需要阻塞寫鎖:一個協程擁有寫鎖時,其他協程寫鎖定需要阻塞;
寫鎖需要阻塞讀鎖:一個協程擁有寫鎖時,其他協程讀鎖定需要阻塞;
讀鎖需要阻塞寫鎖:一個協程擁有讀鎖時,其他協程寫鎖定需要阻塞;
讀鎖不能阻塞讀鎖:一個協程擁有讀鎖時,其他協程也可以擁有讀鎖。
Go atomic包是最輕量級的鎖(也稱無鎖結構),可以在不形成臨界區和創建互斥量的情況下完成并發安全的值替換操作,不過這個包只支持int32/int64/uint32/uint64/uintptr這幾種數據類型的一些基礎操作(增減、交換、載入、存儲等)
當我們想要對**某個變量**并發安全的修改,除了使用官方提供的 `mutex`,還可以使用 sync/atomic 包的原子操作,它能夠保證對變量的讀取或修改期間不被其他的協程所影響。
atomic 包提供的原子操作能夠確保任一時刻只有一個goroutine對變量進行操作,善用 atomic 能夠避免程序中出現大量的鎖操作。
**常見操作:**
- 增減Add AddInt32 AddInt64 AddUint32 AddUint64 AddUintptr
- 載入Load LoadInt32 LoadInt64 LoadPointer LoadUint32 LoadUint64 LoadUintptr
- 比較并交換CompareAndSwap CompareAndSwapInt32...
- 交換Swap SwapInt32...
- 存儲Store StoreInt32...
原子操作由底層硬件支持,而鎖是基于原子操作+信號量完成的。若實現相同的功能,前者通常會更有效率
原子操作是單個指令的互斥操作;互斥鎖/讀寫鎖是一種數據結構,可以完成臨界區(多個指令)的互斥操作,擴大原子操作的范圍
原子操作是無鎖操作,屬于樂觀鎖;說起鎖的時候,一般屬于悲觀鎖
原子操作存在于各個指令/語言層級,比如“機器指令層級的原子操作”,“匯編指令層級的原子操作”,“Go語言層級的原子操作”等。
鎖也存在于各個指令/語言層級中,比如“機器指令層級的鎖”,“匯編指令層級的鎖”,“Go語言層級的鎖”等
g本質是一個數據結構,真正讓 goroutine 運行起來的是調度器
type g struct {
goid int64 // 唯一的goroutine的ID
sched gobuf // goroutine切換時,用于保存g的上下文
stack stack // 棧
gopc // pc of go statement that created this goroutine
startpc uintptr // pc of goroutine function ...
}
type gobuf struct { //運行時寄存器
sp uintptr // 棧指針位置
pc uintptr // 運行到的程序位置
g guintptr // 指向 goroutine
ret uintptr // 保存系統調用的返回值 ...
}
type stack struct { //運行時棧
lo uintptr // 棧的下界內存地址
hi uintptr // 棧的上界內存地址
}
內存占用:
創建一個 goroutine 的棧內存消耗為 2 KB,實際運行過程中,如果棧空間不夠用,會自動進行擴容。創建一個 thread 則需要消耗 1 MB 棧內存。
創建和銷毀:
Thread 創建和銷毀需要陷入內核,系統調用。而 goroutine 因為是由 Go runtime 負責管理的,創建和銷毀的消耗非常小,是用戶級。
切換:
當 threads 切換時,需要保存各種寄存器,而 goroutines 切換只需保存三個寄存器:Program Counter, Stack Pointer and BP。一般而言,線程切換會消耗 1000-1500 ns,Goroutine 的切換約為 200 ns,因此,goroutines 切換成本比 threads 要小得多。
泄露原因
Goroutine 內進行channel/mutex 等讀寫操作被一直阻塞。
Goroutine 內的業務邏輯進入死循環,資源一直無法釋放。
Goroutine 內的業務邏輯進入長時間等待,有不斷新增的 Goroutine 進入等待
泄露場景
channel 如果忘記初始化,那么無論你是讀,還是寫操作,都會造成阻塞。
channel 發送數量 超過 channel接收數量,就會造成阻塞
channel 接收數量 超過 channel發送數量,也會造成阻塞
http request body未關閉,goroutine不會退出
互斥鎖忘記解鎖
sync.WaitGroup使用不當
如何排查
單個函數:調用 `runtime.NumGoroutine` 方法來打印 執行代碼前后Goroutine 的運行數量,進行前后比較,就能知道有沒有泄露了。
生產/測試環境:使用`PProf`實時監測Goroutine的數量
package main
import (
"net/http"
_ "net/http/pprof"
)
func main() {
for i := 0; i < 100; i++ {
go func() {
select {}
}()
}
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
select {}
}
執行程序之后,命令運行以下命令,會自動打開瀏覽器顯示一系列目前還看不懂的圖,提示Could not execute dot; may need to install graphviz.則需要安裝graphviz,需要python環境
go tool pprof -http=:1248 http://127.0.0.1:6060/debug/pprof/goroutine
在開發過程中,如果不對goroutine加以控制而進行濫用的話,可能會導致服務整體崩潰。比如耗盡系統資源導致程序崩潰,或者CPU使用率過高導致系統忙不過來。
解決方案:
有緩沖channel:利用緩沖滿時發送阻塞的特性
無緩沖channel:任務發送和執行分離,指定消費者并發協程數
M個線程對應N個內核線程
優點:
- 能夠利用多核
- 上下文切換成本低
- 如果進程中的一個線程被阻塞,不會阻塞其他線程,是能夠切換同一進程內的其他線程繼續執行
G:Goroutine
M: 線程
P: Processor 本地隊列
GM模型:
2012年前的調度器模型,使用了4年果斷被拋棄,缺點如下:
1. 創建、銷毀、調度G都需要每個M獲取鎖,這就形成了激烈的鎖競爭。
2. M轉移G會造成延遲和額外的系統負載。比如當G中包含創建新協程的時候,M創建了G’,為了繼續執行G,需要把G’交給M’執行,也造成了很差的局部性,因為G’和G是相關的,最好放在M上執行,而不是其他M'。
3. 系統調用(CPU在M之間的切換)導致頻繁的線程阻塞和取消阻塞操作增加了系統開銷。
GMP模型:
P的數量:
由啟動時環境變量`$GOMAXPROCS`或者是由`runtime`的方法`GOMAXPROCS()`決定
M的數量:
go語言本身的限制:go程序啟動時,會設置M的最大數量,默認10000.但是內核很難支持這么多的線程數
runtime/debug中的SetMaxThreads函數,設置M的最大數量
一個M阻塞了,會創建新的M。
P何時創建:在確定了P的最大數量n后,運行時系統會根據這個數量創建n個P。
M何時創建:沒有足夠的M來關聯P并運行其中的可運行的G。比如所有的M此時都阻塞住了,而P中還有很多就緒任務,就會去尋找空閑的M,而沒有空閑的,就會去創建新的M。
全場景解析:
1.P擁有G1,M1獲取P后開始運行G1,G1創建了G2,為了局部性G2優先加入到P1的本地隊列。
2.G1運行完成后,M上運行的goroutine切換為G0,G0負責調度時協程的切換。從P的本地隊列取G2,從G0切換到G2,并開始運行G2。實現了線程M1的復用。
3.假設每個P的本地隊列只能存4個G。G2要創建了6個G,前4個G(G3, G4, G5, G6)已經加入p1的本地隊列,p1本地隊列滿了。
4.G2在創建G7的時候,發現P1的本地隊列已滿,需要執行負載均衡(把P1中本地隊列中前一半的G,還有新創建G轉移到全局隊列),這些G被轉移到全局隊列時,會被打亂順序
5.G2創建G8時,P1的本地隊列未滿,所以G8會被加入到P1的本地隊列。
6.在創建G時,運行的G會嘗試喚醒其他空閑的P和M組合去執行。假定G2喚醒了M2,M2綁定了P2,并運行G0,但P2本地隊列沒有G,M2此時為自旋線程
7.M2嘗試從全局隊列取一批G放到P2的本地隊列,至少從全局隊列取1個g,但每次不要從全局隊列移動太多的g到p本地隊列,給其他p留點。
8.假設G2一直在M1上運行,經過2輪后,M2已經把G7、G4從全局隊列獲取到了P2的本地隊列并完成運行,全局隊列和P2的本地隊列都空了,那m就要執行work stealing(偷取):從其他有G的P哪里偷取一半G過來,放到自己的P本地隊列。P2從P1的本地隊列尾部取一半的G
9.G1本地隊列G5、G6已經被其他M偷走并運行完成,當前M1和M2分別在運行G2和G8,M3和M4沒有goroutine可以運行,M3和M4處于自旋狀態,它們不斷尋找goroutine。系統中最多有GOMAXPROCS個自旋的線程,多余的沒事做線程會讓他們休眠。
10.假定當前除了M3和M4為自旋線程,還有M5和M6為空閑的線程,G8創建了G9,G8進行了阻塞的系統調用,M2和P2立即解綁,P2會執行以下判斷:如果P2本地隊列有G、全局隊列有G或有空閑的M,P2都會立馬喚醒1個M和它綁定,否則P2則會加入到空閑P列表,等待M來獲取可用的p。
11.G8創建了G9,假如G8進行了非阻塞系統調用。M2和P2會解綁,但M2會記住P2,然后G8和M2進入系統調用狀態。當G8和M2退出系統調用時,會嘗試獲取P2,如果無法獲取,則獲取空閑的P,如果依然沒有,G8會被記為可運行狀態,并加入到全局隊列,M2因為沒有P的綁定而變成休眠狀態
當線程M?可運?的G時,嘗試從其他M綁定的P偷取G,減少空轉,提高了線程利用率(避免閑著不干活)。
當從本線程綁定 P 本地 隊列、全局G隊列、netpoller都找不到可執行的 g,會從別的 P 里竊取G并放到當前P上面。
從netpoller 中拿到的G是_Gwaiting狀態( 存放的是因為網絡IO被阻塞的G),從其它地方拿到的G是_Grunnable狀態
從全局隊列取的G數量:N = min(len(GRQ)/GOMAXPROCS + 1, len(GRQ/2)) (根據GOMAXPROCS負載均衡)
從其它P本地隊列竊取的G數量:N = len(LRQ)/2(平分)
也稱為P分離機制,當本線程 M 因為 G 進行的系統調用阻塞時,線程釋放綁定的 P,把 P 轉移給其他空閑的 M 執行,也提高了線程利用率(避免站著茅坑不拉shi)。
有 2 種方式可以查看一個程序的調度GMP信息,分別是go tool trace和GODEBUG
額,這個不太了解!
好的你回去等通知吧!
編譯器會根據變量是否被外部引用來決定是否逃逸:
如果函數外部沒有引用,則優先放到棧中;
如果函數外部存在引用,則必定放到堆中;
如果棧上放不下,則必定放到堆上;
案例:
指針逃逸:函數返回值為局部變量的指針,函數雖然退出了,但是因為指針的存在,指向的內存不能隨著函數結束而回收,因此只能分配在堆上。
棧空間不足:當棧空間足夠時,不會發生逃逸,但是當變量過大時,已經完全超過棧空間的大小時,將會發生逃逸到堆上分配內存。局部變量s占用內存過大,編譯器會將其分配到堆上
變量大小不確定:編譯期間無法確定slice的長度,這種情況為了保證內存的安全,編譯器也會觸發逃逸,在堆上進行分配內存
動態類型:動態類型就是編譯期間不確定參數的類型、參數的長度也不確定的情況下就會發生逃逸
閉包引用對象:閉包函數中局部變量i在后續函數是繼續使用的,編譯器將其分配到堆上
總結:
1. 棧上分配內存比在堆中分配內存效率更高
2. 棧上分配的內存不需要 GC 處理,而堆需要
3. 逃逸分析目的是決定內分配地址是棧還是堆
4. 逃逸分析在編譯階段完成
因為無論變量的大小,只要是指針變量都會在堆上分配,所以對于小變量我們還是使用傳值效率(而不是傳指針)更高一點。
什么是內存對齊
為了能讓CPU可以更快的存取到各個字段,Go編譯器會幫你把struct結構體做數據的對齊。所謂的數據對齊,是指內存地址是所存儲數據大小(按字節為單位)的整數倍,以便CPU可以一次將該數據從內存中讀取出來。編譯器通過在結構體的各個字段之間填充一些空白已達到對齊的目的。存在內存空間的浪費,實際上是空間換時間
對齊原則:
1. 結構體變量中成員的偏移量必須是成員大小的整數倍
2. 整個結構體的地址必須是最大字節的整數倍
GC如何調優在應用程序中會使用到兩種內存,分別為堆(Heap)和棧(Stack),GC負責回收堆內存,而不負責回收棧中的內存
常用GC算法
1.引用計數:python,swift,php
2.分代收集:Java
3.標記清除:GO 三色標記法+混合屏障 停頓時間在0.5ms左右
1.控制內存分配的速度,限制 Goroutine 的數量,提高賦值器 mutator 的 CPU 利用率(降低GC的CPU利用率)
2.少量使用+連接string
3.slice提前分配足夠的內存來降低擴容帶來的拷貝
4.避免map key對象過多,導致掃描時間增加
5.變量復用,減少對象分配,例如使用 sync.Pool 來復用需要頻繁創建臨時對象、使用全局變量等
6.增大 GOGC 的值,降低 GC 的運行頻率 (不太用這個)
1. GODEBUG='gctrace=1' go run main.go
2. go tool trace trace.out
3. debug.ReadGCStats
4. runtime.ReadMemStats
額,這個不太了解!
好的你回去等通知吧!
go run -race main.go
好無聊的面試題,正常人誰這么寫代碼
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func dog(dogChan chan bool, catChan chan bool) {
i := 0
for {
select {
case <-dogChan:
fmt.Println("dog", i)
i++
catChan <- true
break
default:
break
}
}
}
func cat(catChan chan bool, fishChan chan bool) {
for {
select {
case <-catChan:
fmt.Println("cat")
fishChan <- true
break
default:
break
}
}
}
func fish(fishChan chan bool, dogChan chan bool) {
i := 0
for {
select {
case <-fishChan:
fmt.Println("fish")
i++ // 計數,打印完之后就溜溜結束了。
if i > 9 {
wg.Done()
return
}
dogChan <- true
break
default:
break
}
}
}
func main() {
dogChan, catChan, fishChan := make(chan bool), make(chan bool), make(chan bool)
wg.Add(1)
go dog(dogChan, catChan)
go cat(catChan, fishChan)
go fish(fishChan, dogChan)
dogChan <- true // 記得這里進行啟動條件,不然就沒法啟動了。
wg.Wait()
}
func main() {
a := [3]int{1, 2, 3}
for k, v := range a {
if k == 0 {
a[0], a[1] = 100, 200
}
a[k] = 100 + v
}
fmt.Print(a) //數組 101 102 103
}
func main() {
a := []int{1, 2, 3}
for k, v := range a {
if k == 0 {
a[0], a[1] = 100, 200
}
a[k] = 100 + v
}
fmt.Print(a) //切片 101 300 103
}
package main
import "fmt"
func main() {
var a uint = 0
var b uint = 1
c := a - b
fmt.Print(c) //18446744073709551615 64位CPU 2^64-1 32位CPU 2^32-1
}
關于“GO面試題及答案有哪些”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識,可以關注億速云行業資訊頻道,小編每天都會為大家更新不同的知識點。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。