溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

GO面試題及答案有哪些

發(fā)布時(shí)間:2022-10-25 13:45:12 來源:億速云 閱讀:167 作者:iii 欄目:編程語言

這篇文章主要介紹“GO面試題及答案有哪些”的相關(guān)知識(shí),小編通過實(shí)際案例向大家展示操作過程,操作方法簡單快捷,實(shí)用性強(qiáng),希望這篇“GO面試題及答案有哪些”文章能幫助大家解決問題。

slice 擴(kuò)容機(jī)制

GO1.17版本及之前
當(dāng)新切片需要的容量cap大于兩倍擴(kuò)容的容量,則直接按照新切片需要的容量擴(kuò)容;
當(dāng)原 slice 容量 < 1024 的時(shí)候,新 slice 容量變成原來的 2 倍;
當(dāng)原 slice 容量 > 1024,進(jìn)入一個(gè)循環(huán),每次容量變成原來的1.25倍,直到大于期望容量。

GO1.18之后
當(dāng)新切片需要的容量cap大于兩倍擴(kuò)容的容量,則直接按照新切片需要的容量擴(kuò)容;
當(dāng)原 slice 容量 < threshold 的時(shí)候,新 slice 容量變成原來的 2 倍;
當(dāng)原 slice 容量 > threshold,進(jìn)入一個(gè)循環(huán),每次容量增加(舊容量+3*threshold)/4。

slice為什么不是線程安全的

slice底層結(jié)構(gòu)并沒有使用加鎖的方式,不支持并發(fā)讀寫

map 底層原理

map 是一個(gè)指針 占用8個(gè)字節(jié)(64位計(jì)算機(jī)),指向hmap結(jié)構(gòu)體,hmap包含多個(gè)bmap數(shù)組(桶) 
type hmap struct { 
    count int  //元素個(gè)數(shù),調(diào)用len(map)時(shí)直接返回 
    flags uint8  //標(biāo)志map當(dāng)前狀態(tài),正在刪除元素、添加元素..... 
    B uint8  //單元(buckets)的對數(shù) B=5表示能容納32個(gè)元素  B隨著map容量增大而變大
    noverflow uint16  //單元(buckets)溢出數(shù)量,如果一個(gè)單元能存8個(gè)key,此時(shí)存儲(chǔ)了9個(gè),溢出了,就需要再增加一個(gè)單元 
    hash0 uint32  //哈希種子 
    buckets unsafe.Pointer //指向單元(buckets)數(shù)組,大小為2^B,可以為nil 
    oldbuckets unsafe.Pointer //擴(kuò)容的時(shí)候,buckets長度會(huì)是oldbuckets的兩倍 
    nevacute uintptr  //指示擴(kuò)容進(jìn)度,小于此buckets遷移完成 
    extra *mapextra //與gc相關(guān) 可選字段 
}

type bmap struct { 
    tophash [bucketCnt]uint8 
} 
//實(shí)際上編譯期間會(huì)生成一個(gè)新的數(shù)據(jù)結(jié)構(gòu)  
type bmap struct { 
    topbits [8]uint8     //key hash值前8位 用于快速定位keys的位置
    keys [8]keytype     //鍵
    values [8]valuetype //值
    pad uintptr 
    overflow uintptr     //指向溢出桶 無符號(hào)整形 優(yōu)化GC
}

map 擴(kuò)容機(jī)制

擴(kuò)容時(shí)機(jī):向 map 插入新 key 的時(shí)候,會(huì)進(jìn)行條件檢測,符合下面這 2 個(gè)條件,就會(huì)觸發(fā)擴(kuò)容
擴(kuò)容條件:
1.超過負(fù)載 map元素個(gè)數(shù) > 6.5(負(fù)載因子) * 桶個(gè)數(shù)
2.溢出桶太多
當(dāng)桶總數(shù)<2^15時(shí),如果溢出桶總數(shù)>=桶總數(shù),則認(rèn)為溢出桶過多
當(dāng)桶總數(shù)>2^15時(shí),如果溢出桶總數(shù)>=2^15,則認(rèn)為溢出桶過多
擴(kuò)容機(jī)制:
雙倍擴(kuò)容:針對條件1,新建一個(gè)buckets數(shù)組,新的buckets大小是原來的2倍,然后舊buckets數(shù)據(jù)搬遷到新的buckets。
等量擴(kuò)容:針對條件2,并不擴(kuò)大容量,buckets數(shù)量維持不變,重新做一遍類似雙倍擴(kuò)容的搬遷動(dòng)作,把松散的鍵值對重新排列一次,使得同一個(gè) bucket 中的 key 排列地更緊密,節(jié)省空間,提高 bucket 利用率,進(jìn)而保證更快的存取。
漸進(jìn)式擴(kuò)容:
插入修改刪除key的時(shí)候,都會(huì)嘗試進(jìn)行搬遷桶的工作,每次都會(huì)檢查oldbucket是否nil,如果不是nil則每次搬遷2個(gè)桶,螞蟻搬家一樣漸進(jìn)式擴(kuò)容

map 遍歷為什么無序

map每次遍歷,都會(huì)從一個(gè)隨機(jī)值序號(hào)的桶,再從其中隨機(jī)的cell開始遍歷,并且擴(kuò)容后,原來桶中的key會(huì)落到其他桶中,本身就會(huì)造成失序
如果想順序遍歷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設(shè)計(jì)就不是用來多個(gè)協(xié)程高并發(fā)訪問的
多個(gè)協(xié)程同時(shí)對map進(jìn)行并發(fā)讀寫,程序會(huì)panic
如果想線程安全,可以使用sync.RWLock 鎖

sync.map
這個(gè)包里面的map實(shí)現(xiàn)了鎖,是線程安全的

Map 如何查找

1.寫保護(hù)機(jī)制
先插hmap的標(biāo)志位flags,如果flags寫標(biāo)志位此時(shí)是1,說明其他協(xié)程正在寫操作,直接panic
2.計(jì)算hash值
key經(jīng)過哈希函數(shù)計(jì)算后,得到64bit(64位CPU)
10010111 | 101011101010110101010101101010101010 | 10010
3.找到hash對應(yīng)的桶
上面64位后5(hmap的B值)位定位所存放的桶
如果當(dāng)前正在擴(kuò)容中,并且定位到舊桶數(shù)據(jù)還未完成遷移,則使用舊的桶
4.遍歷桶查找
上面64位前8位用來在tophash數(shù)組查找快速判斷key是否在當(dāng)前的桶中,如果不在需要去溢出桶查找  
5.返回key對應(yīng)的指針

Map 沖突解決方式

GO采用鏈地址法解決沖突,具體就是插入key到map中時(shí),當(dāng)key定位的桶填滿8個(gè)元素后,將會(huì)創(chuàng)建一個(gè)溢出桶,并且將溢出桶插入當(dāng)前桶的所在鏈表尾部

Map 負(fù)載因子為什么是6.5

負(fù)載因子 = 哈希表存儲(chǔ)的元素個(gè)數(shù) / 桶個(gè)數(shù)
Go 官方發(fā)現(xiàn):裝載因子越大,填入的元素越多,空間利用率就越高,但發(fā)生哈希沖突的幾率就變大。
            裝載因子越小,填入的元素越少,沖突發(fā)生的幾率減小,但空間浪費(fèi)也會(huì)變得更多,而且還會(huì)提高擴(kuò)容操作的次數(shù)
Go 官方取了一個(gè)相對適中的值,把 Go 中的 map 的負(fù)載因子硬編碼為 6.5,這就是 6.5 的選擇緣由。
這意味著在 Go 語言中,當(dāng) map存儲(chǔ)的元素個(gè)數(shù)大于或等于 6.5 * 桶個(gè)數(shù) 時(shí),就會(huì)觸發(fā)擴(kuò)容行為。

Map 和 Sync.Map 哪個(gè)性能好

type Map struct {
    mu Mutex
    read atomic.Value
    dirty map[interface()]*entry
    misses int
}
對比原始map:
和原始map+RWLock的實(shí)現(xiàn)并發(fā)的方式相比,減少了加鎖對性能的影響。它做了一些優(yōu)化:可以無鎖訪問read map,而且會(huì)優(yōu)先操作read map,倘若只操作read map就可以滿足要求,那就不用去操作write map(dirty),所以在某些特定場景中它發(fā)生鎖競爭的頻率會(huì)遠(yuǎn)遠(yuǎn)小于map+RWLock的實(shí)現(xiàn)方式
優(yōu)點(diǎn):
適合讀多寫少的場景
缺點(diǎn):
寫多的場景,會(huì)導(dǎo)致 read map 緩存失效,需要加鎖,沖突變多,性能急劇下降

Channel 底層實(shí)現(xiàn)原理

通過var聲明或者make函數(shù)創(chuàng)建的channel變量是一個(gè)存儲(chǔ)在函數(shù)棧幀上的指針,占用8個(gè)字節(jié),指向堆上的hchan結(jié)構(gòu)體
type hchan struct {
    closed   uint32   // channel是否關(guān)閉的標(biāo)志
    elemtype *_type   // channel中的元素類型
    // channel分為無緩沖和有緩沖兩種。
    // 對于有緩沖的channel存儲(chǔ)數(shù)據(jù),使用了 ring buffer(環(huán)形緩沖區(qū)) 來緩存寫入的數(shù)據(jù),本質(zhì)是循環(huán)數(shù)組
    // 為啥是循環(huán)數(shù)組?普通數(shù)組不行嗎,普通數(shù)組容量固定更適合指定的空間,彈出元素時(shí),普通數(shù)組需要全部都前移
    // 當(dāng)下標(biāo)超過數(shù)組容量后會(huì)回到第一個(gè)位置,所以需要有兩個(gè)字段記錄當(dāng)前讀和寫的下標(biāo)位置
    buf      unsafe.Pointer // 指向底層循環(huán)數(shù)組的指針(環(huán)形緩沖區(qū))
    qcount   uint           // 循環(huán)數(shù)組中的元素?cái)?shù)量
    dataqsiz uint           // 循環(huán)數(shù)組的長度
    elemsize uint16                 // 元素的大小
    sendx    uint           // 下一次寫下標(biāo)的位置
    recvx    uint           // 下一次讀下標(biāo)的位置
    // 嘗試讀取channel或向channel寫入數(shù)據(jù)而被阻塞的goroutine
    recvq    waitq  // 讀等待隊(duì)列
    sendq    waitq  // 寫等待隊(duì)列
    lock mutex //互斥鎖,保證讀寫channel時(shí)不存在并發(fā)競爭問題
}
等待隊(duì)列:
雙向鏈表,包含一個(gè)頭結(jié)點(diǎn)和一個(gè)尾結(jié)點(diǎn)
每個(gè)節(jié)點(diǎn)是一個(gè)sudog結(jié)構(gòu)體變量,記錄哪個(gè)協(xié)程在等待,等待的是哪個(gè)channel,等待發(fā)送/接收的數(shù)據(jù)在哪里
type waitq struct {
    first *sudog
    last  *sudog
}

type sudog struct {
    g *g
    next *sudog
    prev *sudog
    elem unsafe.Pointer 
    c        *hchan 
    ...
}
創(chuàng)建時(shí):
創(chuàng)建時(shí)會(huì)做一些檢查:
-   元素大小不能超過 64K
-   元素的對齊大小不能超過 maxAlign 也就是 8 字節(jié)
-   計(jì)算出來的內(nèi)存是否超過限制

創(chuàng)建時(shí)的策略:
-   如果是無緩沖的 channel,會(huì)直接給 hchan 分配內(nèi)存
-   如果是有緩沖的 channel,并且元素不包含指針,那么會(huì)為 hchan 和底層數(shù)組分配一段連續(xù)的地址
-   如果是有緩沖的 channel,并且元素包含指針,那么會(huì)為 hchan 和底層數(shù)組分別分配地址
發(fā)送時(shí):
-   如果 channel 的讀等待隊(duì)列存在接收者goroutine
-   將數(shù)據(jù)**直接發(fā)送**給第一個(gè)等待的 goroutine, **喚醒接收的 goroutine**
-   如果 channel 的讀等待隊(duì)列不存在接收者goroutine
-   如果循環(huán)數(shù)組buf未滿,那么將會(huì)把數(shù)據(jù)發(fā)送到循環(huán)數(shù)組buf的隊(duì)尾
-   如果循環(huán)數(shù)組buf已滿,這個(gè)時(shí)候就會(huì)走阻塞發(fā)送的流程,將當(dāng)前 goroutine 加入寫等待隊(duì)列,并**掛起等待喚醒**
接收時(shí):
-   如果 channel 的寫等待隊(duì)列存在發(fā)送者goroutine
-   如果是無緩沖 channel,**直接**從第一個(gè)發(fā)送者goroutine那里把數(shù)據(jù)拷貝給接收變量,**喚醒發(fā)送的 goroutine**
-   如果是有緩沖 channel(已滿),將循環(huán)數(shù)組buf的隊(duì)首元素拷貝給接收變量,將第一個(gè)發(fā)送者goroutine的數(shù)據(jù)拷貝到 buf循環(huán)數(shù)組隊(duì)尾,**喚醒發(fā)送的 goroutine**
-   如果 channel 的寫等待隊(duì)列不存在發(fā)送者goroutine
-   如果循環(huán)數(shù)組buf非空,將循環(huán)數(shù)組buf的隊(duì)首元素拷貝給接收變量
-   如果循環(huán)數(shù)組buf為空,這個(gè)時(shí)候就會(huì)走阻塞接收的流程,將當(dāng)前 goroutine 加入讀等待隊(duì)列,并**掛起等待喚醒**

Channel有什么特點(diǎn)

channel有2種類型:無緩沖、有緩沖
channel有3種模式:寫操作模式(單向通道)、讀操作模式(單向通道)、讀寫操作模式(雙向通道)
寫操作模式    make(chan<- int)
讀操作模式    make(<-chan int)
讀寫操作模式    make(chan int)

channel有3種狀態(tài):未初始化、正常、關(guān)閉

操作\狀態(tài)未初始化關(guān)閉正常
關(guān)閉panicpanic正常
發(fā)送永遠(yuǎn)阻塞導(dǎo)致死鎖panic阻塞或者成功發(fā)送
接收永遠(yuǎn)阻塞導(dǎo)致死鎖緩沖區(qū)為空則為零值, 否則可以繼續(xù)讀阻塞或者成功接收

注意點(diǎn):

一個(gè) channel不能多次關(guān)閉,會(huì)導(dǎo)致painc
如果多個(gè) goroutine 都監(jiān)聽同一個(gè) channel,那么 channel 上的數(shù)據(jù)都可能隨機(jī)被某一個(gè) goroutine 取走進(jìn)行消費(fèi)
如果多個(gè) goroutine 監(jiān)聽同一個(gè) channel,如果這個(gè) channel 被關(guān)閉,則所有 goroutine 都能收到退出信號(hào)

Channel為什么是線程安全的

不同協(xié)程通過channel進(jìn)行通信,本身的使用場景就是多線程,為了保證數(shù)據(jù)的一致性,必須實(shí)現(xiàn)線程安全
channel的底層實(shí)現(xiàn)中,hchan結(jié)構(gòu)體中采用Mutex鎖來保證數(shù)據(jù)讀寫安全。在對循環(huán)數(shù)組buf中的數(shù)據(jù)進(jìn)行入隊(duì)和出隊(duì)操作時(shí),必須先獲取互斥鎖,才能操作channel數(shù)據(jù)

Channel發(fā)送和接收什么情況下會(huì)死鎖?

func deadlock1() {    //無緩沖channel只寫不讀
    ch := make(chan int) 
    ch <- 3 //  這里會(huì)發(fā)生一直阻塞的情況,執(zhí)行不到下面一句
}
func deadlock2() { //無緩沖channel讀在寫后面
    ch := make(chan int)
    ch <- 3  //  這里會(huì)發(fā)生一直阻塞的情況,執(zhí)行不到下面一句
    num := <-ch
    fmt.Println("num=", num)
}
func deadlock3() { //無緩沖channel讀在寫后面
    ch := make(chan int)
    ch <- 100 //  這里會(huì)發(fā)生一直阻塞的情況,執(zhí)行不到下面一句
    go func() {
        num := <-ch
        fmt.Println("num=", num)
    }()
    time.Sleep(time.Second)
}
func deadlock3() {    //有緩沖channel寫入超過緩沖區(qū)數(shù)量
    ch := make(chan int, 3)
    ch <- 3
    ch <- 4
    ch <- 5
    ch <- 6  //  這里會(huì)發(fā)生一直阻塞的情況
}
func deadlock4() {    //空讀
    ch := make(chan int)
    // ch := make(chan int, 1)
    fmt.Println(<-ch)  //  這里會(huì)發(fā)生一直阻塞的情況
}
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
        }
    }
}

互斥鎖實(shí)現(xiàn)原理

Go sync包提供了兩種鎖類型:互斥鎖sync.Mutex 和 讀寫互斥鎖sync.RWMutex,都屬于悲觀鎖。
鎖的實(shí)現(xiàn)一般會(huì)依賴于原子操作、信號(hào)量,通過atomic 包中的一些原子操作來實(shí)現(xiàn)鎖的鎖定,通過信號(hào)量來實(shí)現(xiàn)線程的阻塞與喚醒

在正常模式下,鎖的等待者會(huì)按照先進(jìn)先出的順序獲取鎖。但是剛被喚起的 Goroutine 與新創(chuàng)建的 Goroutine 競爭時(shí),大概率會(huì)獲取不到鎖,在這種情況下,這個(gè)被喚醒的 Goroutine 會(huì)加入到等待隊(duì)列的前面。 如果一個(gè)等待的 Goroutine 超過1ms 沒有獲取鎖,那么它將會(huì)把鎖轉(zhuǎn)變?yōu)轲囸I模式。
Go在1.9中引入優(yōu)化,目的保證互斥鎖的公平性。在饑餓模式中,互斥鎖會(huì)直接交給等待隊(duì)列最前面的 Goroutine。新的 Goroutine 在該狀態(tài)下不能獲取鎖、也不會(huì)進(jìn)入自旋狀態(tài),它們只會(huì)在隊(duì)列的末尾等待。如果一個(gè) Goroutine 獲得了互斥鎖并且它在隊(duì)列的末尾或者它等待的時(shí)間少于 1ms,那么當(dāng)前的互斥鎖就會(huì)切換回正常模式。

互斥鎖允許自旋的條件?

線程沒有獲取到鎖時(shí)常見有2種處理方式:
-   一種是沒有獲取到鎖的線程就一直循環(huán)等待判斷該資源是否已經(jīng)釋放鎖,這種鎖也叫做自旋鎖,它不用將線程阻塞起來, 適用于并發(fā)低且程序執(zhí)行時(shí)間短的場景,缺點(diǎn)是cpu占用較高
-   另外一種處理方式就是把自己阻塞起來,會(huì)釋放CPU給其他線程,內(nèi)核會(huì)將線程置為「睡眠」?fàn)顟B(tài),等到鎖被釋放后,內(nèi)核會(huì)在合適的時(shí)機(jī)喚醒該線程,適用于高并發(fā)場景,缺點(diǎn)是有線程上下文切換的開銷
Go語言中的Mutex實(shí)現(xiàn)了自旋與阻塞兩種場景,當(dāng)滿足不了自旋條件時(shí),就會(huì)進(jìn)入阻塞
**允許自旋的條件:**
1.  鎖已被占用,并且鎖不處于饑餓模式。
2.  積累的自旋次數(shù)小于最大自旋次數(shù)(active_spin=4)。
3.  cpu 核數(shù)大于 1。
4.  有空閑的 P。
5.  當(dāng)前 goroutine 所掛載的 P 下,本地待運(yùn)行隊(duì)列為空。

讀寫鎖實(shí)現(xiàn)原理

讀寫鎖的底層是基于互斥鎖實(shí)現(xiàn)的。
寫鎖需要阻塞寫鎖:一個(gè)協(xié)程擁有寫鎖時(shí),其他協(xié)程寫鎖定需要阻塞;
寫鎖需要阻塞讀鎖:一個(gè)協(xié)程擁有寫鎖時(shí),其他協(xié)程讀鎖定需要阻塞;
讀鎖需要阻塞寫鎖:一個(gè)協(xié)程擁有讀鎖時(shí),其他協(xié)程寫鎖定需要阻塞;
讀鎖不能阻塞讀鎖:一個(gè)協(xié)程擁有讀鎖時(shí),其他協(xié)程也可以擁有讀鎖。

原子操作有哪些

Go atomic包是最輕量級(jí)的鎖(也稱無鎖結(jié)構(gòu)),可以在不形成臨界區(qū)和創(chuàng)建互斥量的情況下完成并發(fā)安全的值替換操作,不過這個(gè)包只支持int32/int64/uint32/uint64/uintptr這幾種數(shù)據(jù)類型的一些基礎(chǔ)操作(增減、交換、載入、存儲(chǔ)等)
當(dāng)我們想要對**某個(gè)變量**并發(fā)安全的修改,除了使用官方提供的 `mutex`,還可以使用 sync/atomic 包的原子操作,它能夠保證對變量的讀取或修改期間不被其他的協(xié)程所影響。
atomic 包提供的原子操作能夠確保任一時(shí)刻只有一個(gè)goroutine對變量進(jìn)行操作,善用 atomic 能夠避免程序中出現(xiàn)大量的鎖操作。
**常見操作:**
-   增減Add     AddInt32 AddInt64 AddUint32 AddUint64 AddUintptr
-   載入Load    LoadInt32 LoadInt64    LoadPointer    LoadUint32    LoadUint64    LoadUintptr    
-   比較并交換CompareAndSwap    CompareAndSwapInt32...
-   交換Swap    SwapInt32...
-   存儲(chǔ)Store    StoreInt32...

原子操作和鎖的區(qū)別

原子操作由底層硬件支持,而鎖是基于原子操作+信號(hào)量完成的。若實(shí)現(xiàn)相同的功能,前者通常會(huì)更有效率
原子操作是單個(gè)指令的互斥操作;互斥鎖/讀寫鎖是一種數(shù)據(jù)結(jié)構(gòu),可以完成臨界區(qū)(多個(gè)指令)的互斥操作,擴(kuò)大原子操作的范圍
原子操作是無鎖操作,屬于樂觀鎖;說起鎖的時(shí)候,一般屬于悲觀鎖
原子操作存在于各個(gè)指令/語言層級(jí),比如“機(jī)器指令層級(jí)的原子操作”,“匯編指令層級(jí)的原子操作”,“Go語言層級(jí)的原子操作”等。
鎖也存在于各個(gè)指令/語言層級(jí)中,比如“機(jī)器指令層級(jí)的鎖”,“匯編指令層級(jí)的鎖”,“Go語言層級(jí)的鎖”等

goroutine的底層實(shí)現(xiàn)原理

g本質(zhì)是一個(gè)數(shù)據(jù)結(jié)構(gòu),真正讓 goroutine 運(yùn)行起來的是調(diào)度器
type g struct { 
    goid int64  // 唯一的goroutine的ID 
    sched gobuf // goroutine切換時(shí),用于保存g的上下文 
    stack stack // 棧 
    gopc // pc of go statement that created this goroutine 
    startpc uintptr  // pc of goroutine function ... 
} 
type gobuf struct {     //運(yùn)行時(shí)寄存器
    sp uintptr  // 棧指針位置 
    pc uintptr  // 運(yùn)行到的程序位置 
    g  guintptr // 指向 goroutine 
    ret uintptr // 保存系統(tǒng)調(diào)用的返回值 ... 
} 
type stack struct {     //運(yùn)行時(shí)棧
    lo uintptr  // 棧的下界內(nèi)存地址 
    hi uintptr  // 棧的上界內(nèi)存地址 
}

goroutine和線程的區(qū)別

內(nèi)存占用:
創(chuàng)建一個(gè) goroutine 的棧內(nèi)存消耗為 2 KB,實(shí)際運(yùn)行過程中,如果??臻g不夠用,會(huì)自動(dòng)進(jìn)行擴(kuò)容。創(chuàng)建一個(gè) thread 則需要消耗 1 MB 棧內(nèi)存。
創(chuàng)建和銷毀:
Thread 創(chuàng)建和銷毀需要陷入內(nèi)核,系統(tǒng)調(diào)用。而 goroutine 因?yàn)槭怯?nbsp;Go runtime 負(fù)責(zé)管理的,創(chuàng)建和銷毀的消耗非常小,是用戶級(jí)。
切換:
當(dāng) threads 切換時(shí),需要保存各種寄存器,而 goroutines 切換只需保存三個(gè)寄存器:Program Counter, Stack Pointer and BP。一般而言,線程切換會(huì)消耗 1000-1500 ns,Goroutine 的切換約為 200 ns,因此,goroutines 切換成本比 threads 要小得多。

goroutine 泄露場景

泄露原因
Goroutine 內(nèi)進(jìn)行channel/mutex 等讀寫操作被一直阻塞。
Goroutine 內(nèi)的業(yè)務(wù)邏輯進(jìn)入死循環(huán),資源一直無法釋放。
Goroutine 內(nèi)的業(yè)務(wù)邏輯進(jìn)入長時(shí)間等待,有不斷新增的 Goroutine 進(jìn)入等待

泄露場景
channel 如果忘記初始化,那么無論你是讀,還是寫操作,都會(huì)造成阻塞。
channel 發(fā)送數(shù)量 超過 channel接收數(shù)量,就會(huì)造成阻塞
channel 接收數(shù)量 超過 channel發(fā)送數(shù)量,也會(huì)造成阻塞
http request body未關(guān)閉,goroutine不會(huì)退出
互斥鎖忘記解鎖
sync.WaitGroup使用不當(dāng)

如何排查
單個(gè)函數(shù):調(diào)用 `runtime.NumGoroutine` 方法來打印 執(zhí)行代碼前后Goroutine 的運(yùn)行數(shù)量,進(jìn)行前后比較,就能知道有沒有泄露了。
生產(chǎn)/測試環(huán)境:使用`PProf`實(shí)時(shí)監(jiān)測Goroutine的數(shù)量

如何查看正在運(yùn)行的goroutine數(shù)量

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 {}
}
執(zhí)行程序之后,命令運(yùn)行以下命令,會(huì)自動(dòng)打開瀏覽器顯示一系列目前還看不懂的圖,提示Could not execute dot; may need to install graphviz.則需要安裝graphviz,需要python環(huán)境
go tool pprof -http=:1248 http://127.0.0.1:6060/debug/pprof/goroutine

如何控制并發(fā)的goroutine數(shù)量?

在開發(fā)過程中,如果不對goroutine加以控制而進(jìn)行濫用的話,可能會(huì)導(dǎo)致服務(wù)整體崩潰。比如耗盡系統(tǒng)資源導(dǎo)致程序崩潰,或者CPU使用率過高導(dǎo)致系統(tǒng)忙不過來。
解決方案:
有緩沖channel:利用緩沖滿時(shí)發(fā)送阻塞的特性
無緩沖channel:任務(wù)發(fā)送和執(zhí)行分離,指定消費(fèi)者并發(fā)協(xié)程數(shù)

GO 線程模型如何實(shí)現(xiàn)

M個(gè)線程對應(yīng)N個(gè)內(nèi)核線程
優(yōu)點(diǎn):
-   能夠利用多核
-   上下文切換成本低
-   如果進(jìn)程中的一個(gè)線程被阻塞,不會(huì)阻塞其他線程,是能夠切換同一進(jìn)程內(nèi)的其他線程繼續(xù)執(zhí)行

GMP和GM模型

G:Goroutine
M: 線程
P: Processor 本地隊(duì)列

GM模型:
2012年前的調(diào)度器模型,使用了4年果斷被拋棄,缺點(diǎn)如下:
1.  創(chuàng)建、銷毀、調(diào)度G都需要每個(gè)M獲取鎖,這就形成了激烈的鎖競爭。
2.  M轉(zhuǎn)移G會(huì)造成延遲和額外的系統(tǒng)負(fù)載。比如當(dāng)G中包含創(chuàng)建新協(xié)程的時(shí)候,M創(chuàng)建了G’,為了繼續(xù)執(zhí)行G,需要把G’交給M’執(zhí)行,也造成了很差的局部性,因?yàn)镚’和G是相關(guān)的,最好放在M上執(zhí)行,而不是其他M'。
3.  系統(tǒng)調(diào)用(CPU在M之間的切換)導(dǎo)致頻繁的線程阻塞和取消阻塞操作增加了系統(tǒng)開銷。

GMP模型:
P的數(shù)量:
由啟動(dòng)時(shí)環(huán)境變量`$GOMAXPROCS`或者是由`runtime`的方法`GOMAXPROCS()`決定
M的數(shù)量:
go語言本身的限制:go程序啟動(dòng)時(shí),會(huì)設(shè)置M的最大數(shù)量,默認(rèn)10000.但是內(nèi)核很難支持這么多的線程數(shù)
runtime/debug中的SetMaxThreads函數(shù),設(shè)置M的最大數(shù)量
一個(gè)M阻塞了,會(huì)創(chuàng)建新的M。

P何時(shí)創(chuàng)建:在確定了P的最大數(shù)量n后,運(yùn)行時(shí)系統(tǒng)會(huì)根據(jù)這個(gè)數(shù)量創(chuàng)建n個(gè)P。
M何時(shí)創(chuàng)建:沒有足夠的M來關(guān)聯(lián)P并運(yùn)行其中的可運(yùn)行的G。比如所有的M此時(shí)都阻塞住了,而P中還有很多就緒任務(wù),就會(huì)去尋找空閑的M,而沒有空閑的,就會(huì)去創(chuàng)建新的M。

全場景解析:
1.P擁有G1,M1獲取P后開始運(yùn)行G1,G1創(chuàng)建了G2,為了局部性G2優(yōu)先加入到P1的本地隊(duì)列。
2.G1運(yùn)行完成后,M上運(yùn)行的goroutine切換為G0,G0負(fù)責(zé)調(diào)度時(shí)協(xié)程的切換。從P的本地隊(duì)列取G2,從G0切換到G2,并開始運(yùn)行G2。實(shí)現(xiàn)了線程M1的復(fù)用。
3.假設(shè)每個(gè)P的本地隊(duì)列只能存4個(gè)G。G2要?jiǎng)?chuàng)建了6個(gè)G,前4個(gè)G(G3, G4, G5, G6)已經(jīng)加入p1的本地隊(duì)列,p1本地隊(duì)列滿了。
4.G2在創(chuàng)建G7的時(shí)候,發(fā)現(xiàn)P1的本地隊(duì)列已滿,需要執(zhí)行負(fù)載均衡(把P1中本地隊(duì)列中前一半的G,還有新創(chuàng)建G轉(zhuǎn)移到全局隊(duì)列),這些G被轉(zhuǎn)移到全局隊(duì)列時(shí),會(huì)被打亂順序
5.G2創(chuàng)建G8時(shí),P1的本地隊(duì)列未滿,所以G8會(huì)被加入到P1的本地隊(duì)列。
6.在創(chuàng)建G時(shí),運(yùn)行的G會(huì)嘗試喚醒其他空閑的P和M組合去執(zhí)行。假定G2喚醒了M2,M2綁定了P2,并運(yùn)行G0,但P2本地隊(duì)列沒有G,M2此時(shí)為自旋線程
7.M2嘗試從全局隊(duì)列取一批G放到P2的本地隊(duì)列,至少從全局隊(duì)列取1個(gè)g,但每次不要從全局隊(duì)列移動(dòng)太多的g到p本地隊(duì)列,給其他p留點(diǎn)。
8.假設(shè)G2一直在M1上運(yùn)行,經(jīng)過2輪后,M2已經(jīng)把G7、G4從全局隊(duì)列獲取到了P2的本地隊(duì)列并完成運(yùn)行,全局隊(duì)列和P2的本地隊(duì)列都空了,那m就要執(zhí)行work stealing(偷取):從其他有G的P哪里偷取一半G過來,放到自己的P本地隊(duì)列。P2從P1的本地隊(duì)列尾部取一半的G
9.G1本地隊(duì)列G5、G6已經(jīng)被其他M偷走并運(yùn)行完成,當(dāng)前M1和M2分別在運(yùn)行G2和G8,M3和M4沒有g(shù)oroutine可以運(yùn)行,M3和M4處于自旋狀態(tài),它們不斷尋找goroutine。系統(tǒng)中最多有GOMAXPROCS個(gè)自旋的線程,多余的沒事做線程會(huì)讓他們休眠。
10.假定當(dāng)前除了M3和M4為自旋線程,還有M5和M6為空閑的線程,G8創(chuàng)建了G9,G8進(jìn)行了阻塞的系統(tǒng)調(diào)用,M2和P2立即解綁,P2會(huì)執(zhí)行以下判斷:如果P2本地隊(duì)列有G、全局隊(duì)列有G或有空閑的M,P2都會(huì)立馬喚醒1個(gè)M和它綁定,否則P2則會(huì)加入到空閑P列表,等待M來獲取可用的p。
11.G8創(chuàng)建了G9,假如G8進(jìn)行了非阻塞系統(tǒng)調(diào)用。M2和P2會(huì)解綁,但M2會(huì)記住P2,然后G8和M2進(jìn)入系統(tǒng)調(diào)用狀態(tài)。當(dāng)G8和M2退出系統(tǒng)調(diào)用時(shí),會(huì)嘗試獲取P2,如果無法獲取,則獲取空閑的P,如果依然沒有,G8會(huì)被記為可運(yùn)行狀態(tài),并加入到全局隊(duì)列,M2因?yàn)闆]有P的綁定而變成休眠狀態(tài)

work stealing 機(jī)制?

當(dāng)線程M?可運(yùn)?的G時(shí),嘗試從其他M綁定的P偷取G,減少空轉(zhuǎn),提高了線程利用率(避免閑著不干活)。
當(dāng)從本線程綁定 P 本地 隊(duì)列、全局G隊(duì)列、netpoller都找不到可執(zhí)行的 g,會(huì)從別的 P 里竊取G并放到當(dāng)前P上面。
從netpoller 中拿到的G是_Gwaiting狀態(tài)( 存放的是因?yàn)榫W(wǎng)絡(luò)IO被阻塞的G),從其它地方拿到的G是_Grunnable狀態(tài)
從全局隊(duì)列取的G數(shù)量:N = min(len(GRQ)/GOMAXPROCS + 1, len(GRQ/2)) (根據(jù)GOMAXPROCS負(fù)載均衡)
從其它P本地隊(duì)列竊取的G數(shù)量:N = len(LRQ)/2(平分)

hand off 機(jī)制?

也稱為P分離機(jī)制,當(dāng)本線程 M 因?yàn)?nbsp;G 進(jìn)行的系統(tǒng)調(diào)用阻塞時(shí),線程釋放綁定的 P,把 P 轉(zhuǎn)移給其他空閑的 M 執(zhí)行,也提高了線程利用率(避免站著茅坑不拉shi)。

如何查看運(yùn)行時(shí)調(diào)度信息? @todo

有 2 種方式可以查看一個(gè)程序的調(diào)度GMP信息,分別是go tool trace和GODEBUG

內(nèi)存分配機(jī)制 @todo

額,這個(gè)不太了解!
好的你回去等通知吧!

內(nèi)存逃逸機(jī)制

編譯器會(huì)根據(jù)變量是否被外部引用來決定是否逃逸:
如果函數(shù)外部沒有引用,則優(yōu)先放到棧中;
如果函數(shù)外部存在引用,則必定放到堆中;
如果棧上放不下,則必定放到堆上;

案例:
指針逃逸:函數(shù)返回值為局部變量的指針,函數(shù)雖然退出了,但是因?yàn)橹羔樀拇嬖冢赶虻膬?nèi)存不能隨著函數(shù)結(jié)束而回收,因此只能分配在堆上。
棧空間不足:當(dāng)??臻g足夠時(shí),不會(huì)發(fā)生逃逸,但是當(dāng)變量過大時(shí),已經(jīng)完全超過??臻g的大小時(shí),將會(huì)發(fā)生逃逸到堆上分配內(nèi)存。局部變量s占用內(nèi)存過大,編譯器會(huì)將其分配到堆上
變量大小不確定:編譯期間無法確定slice的長度,這種情況為了保證內(nèi)存的安全,編譯器也會(huì)觸發(fā)逃逸,在堆上進(jìn)行分配內(nèi)存
動(dòng)態(tài)類型:動(dòng)態(tài)類型就是編譯期間不確定參數(shù)的類型、參數(shù)的長度也不確定的情況下就會(huì)發(fā)生逃逸
閉包引用對象:閉包函數(shù)中局部變量i在后續(xù)函數(shù)是繼續(xù)使用的,編譯器將其分配到堆上

總結(jié):
1.  棧上分配內(nèi)存比在堆中分配內(nèi)存效率更高
2.  棧上分配的內(nèi)存不需要 GC 處理,而堆需要
3.  逃逸分析目的是決定內(nèi)分配地址是棧還是堆
4.  逃逸分析在編譯階段完成
因?yàn)闊o論變量的大小,只要是指針變量都會(huì)在堆上分配,所以對于小變量我們還是使用傳值效率(而不是傳指針)更高一點(diǎn)。

GO內(nèi)存對齊機(jī)制

什么是內(nèi)存對齊
為了能讓CPU可以更快的存取到各個(gè)字段,Go編譯器會(huì)幫你把struct結(jié)構(gòu)體做數(shù)據(jù)的對齊。所謂的數(shù)據(jù)對齊,是指內(nèi)存地址是所存儲(chǔ)數(shù)據(jù)大?。ò醋止?jié)為單位)的整數(shù)倍,以便CPU可以一次將該數(shù)據(jù)從內(nèi)存中讀取出來。編譯器通過在結(jié)構(gòu)體的各個(gè)字段之間填充一些空白已達(dá)到對齊的目的。存在內(nèi)存空間的浪費(fèi),實(shí)際上是空間換時(shí)間
對齊原則:
1.  結(jié)構(gòu)體變量中成員的偏移量必須是成員大小的整數(shù)倍
2.  整個(gè)結(jié)構(gòu)體的地址必須是最大字節(jié)的整數(shù)倍

GC實(shí)現(xiàn)原理 @todo

在應(yīng)用程序中會(huì)使用到兩種內(nèi)存,分別為堆(Heap)和棧(Stack),GC負(fù)責(zé)回收堆內(nèi)存,而不負(fù)責(zé)回收棧中的內(nèi)存
常用GC算法
1.引用計(jì)數(shù):python,swift,php
2.分代收集:Java
3.標(biāo)記清除:GO 三色標(biāo)記法+混合屏障 停頓時(shí)間在0.5ms左右

GC如何調(diào)優(yōu)

1.控制內(nèi)存分配的速度,限制 Goroutine 的數(shù)量,提高賦值器 mutator 的 CPU 利用率(降低GC的CPU利用率)
2.少量使用+連接string
3.slice提前分配足夠的內(nèi)存來降低擴(kuò)容帶來的拷貝
4.避免map key對象過多,導(dǎo)致掃描時(shí)間增加
5.變量復(fù)用,減少對象分配,例如使用 sync.Pool 來復(fù)用需要頻繁創(chuàng)建臨時(shí)對象、使用全局變量等
6.增大 GOGC 的值,降低 GC 的運(yùn)行頻率 (不太用這個(gè))

如何查看GC信息

1. GODEBUG='gctrace=1' go run main.go
2. go tool trace trace.out
3. debug.ReadGCStats
4. runtime.ReadMemStats

Go 有哪些并發(fā)同步原語? @todo

額,這個(gè)不太了解!
好的你回去等通知吧!

Go 如何排查數(shù)據(jù)競爭問題?

go run -race main.go

Go 限制協(xié)程數(shù)、按順序打印cat、dog、fish各100次

好無聊的面試題,正常人誰這么寫代碼

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++ // 計(jì)數(shù),打印完之后就溜溜結(jié)束了。
            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 // 記得這里進(jìn)行啟動(dòng)條件,不然就沒法啟動(dòng)了。
    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)  //數(shù)組    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
}

關(guān)于“GO面試題及答案有哪些”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí),可以關(guān)注億速云行業(yè)資訊頻道,小編每天都會(huì)為大家更新不同的知識(shí)點(diǎn)。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

go
AI