天天看點

【Golang開發面經】米哈遊(一輪遊)

文章目錄

  • ​​寫在前面​​
  • ​​筆試​​
  • ​​一面​​
  • ​​線程和協程有什麼差別?各自有什麼優缺點?​​
  • ​​程序之間如何進行通信?​​
  • ​​什麼是信号,信号量是如何實作的?​​
  • ​​講講Go裡面的GMP模型?​​
  • ​​Go的GMP模型​​
  • ​​map用過吧?怎麼對map進行排序?​​
  • ​​講講map底層?為什麼是無序的?​​
  • ​​知道TCP連接配接吧?三次握手的目的是什麼?​​
  • ​​那四次揮手有了解過嗎?為什麼是四次?​​
  • ​​什麼是粘包和拆包?為什麼會出現?​​
  • ​​怎麼解決?​​
  • ​​什麼是事務?​​
  • ​​那 redis 支援事務嗎?​​
  • ​​算法:手撕連結清單,要求遞歸實作。​​
  • ​​算法:忘記了。。好像是中等題,反正不難,也不簡單。​​
  • ​​二面​​

寫在前面

米哈遊 面試下來感覺還行吧,挺注重基礎的,面試官水準也很高。但是感覺不是在招人的樣子,我有好多同學都是履歷挂(

筆試

無筆試,很奇怪(

一面

線程和協程有什麼差別?各自有什麼優缺點?

程序 線程
系統中正在運作的一個應用程式 系統配置設定處理器時間資源的基本單元
程式一旦運作就是程序 程序之内獨立執行的一個單元執行流
資源配置設定的最小機關 程式執行的最小機關

程序有​

​獨立的位址空間​

​​,一個程序崩潰後,在​

​保護模式下不會對其它程序産生影響​

​,而線程隻是一個程序中的不同執行路徑。

線程有自己的堆棧和局部變量,但線程​

​沒有單獨的位址空間​

​​,一個線程死掉就等于整個程序死掉,是以多程序的程式要比多線程的程式健壯,但在​

​程序切換​

​​時,耗費資源較大,效率要差一些。但對于一些 ​

​要求同時進行并且又要共享某些變量的并發操作,隻能用線程,不能用程序​

程序之間如何進行通信?

管道、消息隊列、共享記憶體、信号量、信号、socket

什麼是信号,信号量是如何實作的?

信号:

在Linux中,為了響應各種事件,提供了幾十種信号,可以通過​​

​kill -l​

​指令檢視。

如果是運作在 shell終端 的程序,可以通過鍵盤組合鍵來給程序發送信号,例如使用Ctrl+C 産生​

​ SIGINT 信号​

​,表示終止程序。

如果是運作在背景的程序,可以通過指令來給程序發送信号,例如使用​

​kill -9 PID​

​​ 産生​

​SIGKILL​

​信号,表示立即結束程序。

信号量:

當使用共享記憶體的通信方式,如果有多個程序同時往共享記憶體寫入資料,有可能先寫的程序的内容被其他程序覆寫了。

是以需要一種保護機制,信号量本質上是一個整型的計數器,用于實作​

​程序間的互斥和同步​

​。

信号量代表着資源的數量,操作信号量的方式有兩種:

  • P操作:這個操作會将信号量減一,相減後信号量如果小于0,則表示資源已經被占用了,程序需要阻塞等待;如果大于等于0,則說明還有資源可用,程序可以正常執行。
  • V操作:這個操作會将信号量加一,相加後信号量如果小于等于0,則表明目前有程序阻塞,于是會将該程序喚醒;如果大于0,則表示目前沒有阻塞的程序。

講講Go裡面的GMP模型?

Go的GMP模型

G:表示goroutine,存儲了goroutine的執行stack資訊、goroutine狀态以及goroutine的任務函數等;另外G對象是​

​可以重用​

​​的。

P:表示邏輯processor,P 的數量決定了系統内最大可并行的 G 的數量(前提:系統的實體cpu核數 >= P的數量);P的最大作用還是其擁有的各種G對象隊列、連結清單、一些cache和狀态。

M:M 代表着真正的執行計算資源,實體 Processor。

G 如果想運作起來必須依賴 P,因為 P 是它的​

​邏輯處理單元​

​​,但是 P 要想真正的運作,他也需要與 M 綁定,這樣才能真正的運作起來,​

​P 和 M 的這種關系就相當于 Linux 系統中的使用者層面的線程和核心的線程是一樣的​

​。
【Golang開發面經】米哈遊(一輪遊)

map用過吧?怎麼對map進行排序?

​go的map不保證有序性,是以按key排序需要取出key,對key排序,再周遊輸出value​

講講map底層?為什麼是無序的?

map 的底層是一個結構體

// Go map 的底層結構體表示
type hmap struct {
    count     int    // map中鍵值對的個數,使用len()可以擷取 
    flags     uint8
    B         uint8  // 哈希桶的數量的log2,比如有8個桶,那麼B=3
    noverflow uint16 // 溢出桶的數量
    hash0     uint32 // 哈希種子

    buckets    unsafe.Pointer // 指向哈希桶數組的指針,數量為 2^B 
    oldbuckets unsafe.Pointer // 擴容時指向舊桶的指針,當擴容時不為nil 
    nevacuate  uintptr        

    extra *mapextra  // 可選字段
}

const (
    bucketCntBits = 3
    bucketCnt     = 1 << bucketCntBits     // 桶數量 1 << 3 = 8
)

// Go map 的一個哈希桶,一個桶最多存放8個鍵值對
type bmap struct {
    // tophash存放了哈希值的最高位元組
    tophash [bucketCnt]uint8
    
    // 在這裡有幾個其它的字段沒有顯示出來,因為k-v的數量類型是不确定的,編譯的時候才會确定
    // keys: 是一個數組,大小為bucketCnt=8,存放Key
    // elems: 是一個數組,大小為bucketCnt=8,存放Value
    // 你可能會想到為什麼不用空接口,空接口可以儲存任意類型。但是空接口底層也是個結構體,中間隔了一層。是以在這裡沒有使用空接口。
    // 注意:之是以将所有key存放在一個數組,将value存放在一個數組,而不是鍵值對的形式,是為了消除例如map[int64]所需的填充整數8(記憶體對齊)
    
    // overflow: 是一個指針,指向溢出桶,當該桶不夠用時,就會使用溢出桶
}      
【Golang開發面經】米哈遊(一輪遊)

當向 map 中存儲一個 kv 時,通過​

​ k 的 hash 值與 buckets 長度取餘​

​,定位到 key 在哪一個bucket中,hash 值的高8位存儲在 bucket 的 tophash[i] 中,用來快速判斷 key是否存在。當一個 bucket 滿時,通過 overflow 指針連結到下一個 bucket。

在源碼​

​runtime.mapiterinit​

​中

func mapiterinit(t *maptype, h *hmap, it *hiter) {
    ...
    it.t = t
    it.h = h
    it.B = h.B
    it.buckets = h.buckets
    if t.bucket.kind&kindNoPointers != 0 {
        h.createOverflow()
        it.overflow = h.extra.overflow
        it.oldoverflow = h.extra.oldoverflow
    }

    r := uintptr(fastrand())
    if h.B > 31-bucketCntBits {
        r += uintptr(fastrand()) << 31
    }
    it.startBucket = r & bucketMask(h.B)
    it.offset = uint8(r >> h.B & (bucketCnt - 1))
    it.bucket = it.startBucket
    ...

    mapiternext(it)
}      

通過對​

​ mapiterinit 方法​

​​閱讀,可得知其主要用途是在 map 進行周遊疊代時進行初始化動作。共有三個形參,用于讀取目前​

​哈希表的類型資訊、目前哈希表的存儲資訊和目前周遊疊代的資料​

源碼中 ​

​fastrand​

...
// decide where to start
r := uintptr(fastrand())
if h.B > 31-bucketCntBits {
    r += uintptr(fastrand()) << 31
}
it.startBucket = r & bucketMask(h.B)
it.offset = uint8(r >> h.B & (bucketCnt - 1))

// iterator state
it.bucket = it.startBucket      

在這段代碼中,它生成了随機數。用于決定從哪裡開始循環疊代。更具體的話就是​

​根據随機數,選擇一個桶位置作為起始點進行周遊疊代。​

是以每次重新​

​ for range map​

​,你見到的結果都是不一樣的。那是因為它的起始位置根本就不固定!

知道TCP連接配接吧?三次握手的目的是什麼?

因為通信的前提是確定雙方都是​

​接收和發送資訊是正常​

​​的。

三次握手是為了建立可靠的資料傳輸通道,

第一次握手就是​​

​讓 接收方 知道 發送方 有發送資訊的能力​

​​ 第二次握手就是​

​讓 發送方 知道 接收方 有發送和接受資訊的能力​

​ 第三次握手就是​

​讓 接收方 知道 發送方 有接受資訊的能力​

那四次揮手有了解過嗎?為什麼是四次?

四次揮手則是為了保證等資料完成的被接收完再關閉連接配接。

既然提到需要保證資料完整的傳輸完,那就需要保證雙方 ​

​都達到關閉連接配接的條件才能斷開。​

第一次揮手:用戶端​

​發起關閉連接配接​

​的請求給服務端;

第二次揮手:服務端收到關閉請求的時候可能這個時候資料還沒發送完,是以服務端會先回複一個​

​确認封包​

​​,表示​

​自己知道用戶端想要關閉連接配接了​

​,但是因為資料還沒傳輸完,是以還需要等待;

第三次揮手:當資料傳輸完了,服務端會主動發送一個​

​ FIN 封包​

​​,告訴用戶端,表示資料​

​已經發送完了​

​​,服務端這邊​

​準備關閉連接配接了​

​。

第四次揮手:當用戶端收到服務端的 FIN 封包過後,會回複一個​

​ ACK 封包​

​,告訴服務端自己知道了,再等待一會就關閉連接配接。

什麼是粘包和拆包?為什麼會出現?

粘包就是兩個或者多個以上的包粘在一起,拆包就是為什麼解決粘包問題。

出現粘包原因有兩個,一個是發送方發送不及時,導緻多個包在發送端粘黏。另一個是接收方接受不及時,導緻包在接收端堆疊導緻。

怎麼解決?

​設計一個帶標頭的應用層封包結構就能解決​

​。標頭定長,以特定标志開頭,裡帶着負載長度,這樣接收側隻要以定長嘗試讀取標頭,再按照標頭裡的負載長度讀取負載就行了,多出來的資料都留在緩沖區裡即可。其實ip封包和tcp封包都是這麼幹的。

什麼是事務?

事務具有​

​原子性、一緻性、隔離性、持久性​

​特性,并且 指将一系列資料操作捆綁成為一個整體進行統一管理,如果某一事務執行成功,則在該事物中進行的所有資料更改均會送出,成為資料庫中的永久組成部分。

那 redis 支援事務嗎?

redis 是不支援事務的,因為 他不支援原子性,但是支援一緻性,是最終一緻性。

算法:手撕連結清單,要求遞歸實作。

算法:忘記了。。好像是中等題,反正不難,也不簡單。

二面

繼續閱讀