Go 面試題二(記憶體、並發、OOP)
來源:Durant Thorvalds
01 new 和 make 的區別?
new:只負責分配記憶體,回傳指向該位址的指標。會為新類型分配一片記憶體、初始化為零值,並回傳型別為*T的位址,相當於&T{}。make:只用於 slice、map、channel 的初始化,回傳的是「引用」(即 slice / map / channel 本身),不是指標。
// new: 分配記憶體,回傳 *T
p := new(int) // *int,指向零值
s := new([]int) // *[]int,指向 nil slice(不常用)
// make: 初始化 slice / map / channel,回傳 T
sl := make([]int, 0, 10) // []int
m := make(map[string]int) // map[string]int
ch := make(chan int) // chan int
02 Go 的面向對象是怎麼實現的?
Go 實現面向對象的兩個關鍵是 struct 和 interface。
封裝
- 同一個 package 內,物件對包內檔案可見。
- 不同 package 時,名稱需大寫開頭才會對外可見(導出)。
繼承
繼承是編譯期特徵,在 struct 內嵌入要「繼承」的類型即可:
type A struct{}
type B struct {
A // 嵌入 A,B 擁有 A 的欄位與方法
}
多態
多態是執行期特徵,透過 interface 實現。類型與介面是鬆耦合的:某個類型的實例可以賦給它實現的任意介面型變數。
type Writer interface {
Write([]byte) (int, error)
}
// 只要類型實現了 Write,就可以賦給 Writer 變數
var w Writer = myType{}
Go 支援多重繼承,即在類型中嵌入多個父類型即可。
03 uint 相減結果會怎樣?
var a uint = 1
var b uint = 2
fmt.Println(a - b)
答案:會發生無符號整數溢出。
- 32 位系統:結果為
2^32 - 1 - 64 位系統:結果為
2^64 - 1
為什麼會變成很大的數?
uint 只能存 0 和正整數,沒有「負數」這個概念。在硬體上減法仍是用二進位運算,1 - 2 的位元結果其實等同於「負一」的二補數表示;但 Go 把這串位元一律當成無符號數解讀,所以不會變成 -1,而是被解讀成「該位元寬度下最大的正整數」(模數運算 wrap around),例如 64 位就是 2^64 - 1。
若需要負數怎麼做?
-
改用有符號型別(最直接):需要可能為負的差值時,用
int/int32/int64。var a, b int = 1, 2fmt.Println(a - b) // -1 -
先轉成有符號再相減:資料本身是
uint時,先轉成夠大的有符號型別再算,並注意溢出(例如int裝不下uint最大值時用int64或先檢查範圍)。var a, b uint = 1, 2diff := int(a) - int(b)fmt.Println(diff) // -1 -
需要「帶正負的差值」且要防溢出:先判斷誰大再決定正負,或使用
int64並在轉換前檢查是否在安全範圍內。var a, b uint64 = 1, 2var diff int64if a >= b {diff = int64(a - b)} else {diff = -int64(b - a)}fmt.Println(diff) // -1
04 Go 有沒有在 main 之前執行的函數?怎麼用?
有,init 函數會在 main 之前執行。
func init() {
// ...
}
init 的特點
- 用來初始化無法用初始化表達式初始化的變數。
- 在程式進入
main前執行,常用於註冊、類似sync.Once的只執行一次邏輯。 - 不能被其他函數呼叫。
- 沒有參數、沒有回傳值。
- 每個 package 可以有多個
init,每個源文件也可以有多個init。 - 同一 package 內多個
init的執行順序,Go 沒有明確定義,寫程式時不要依賴其順序。 - 不同 package 的
init依「包導入的依賴關係」決定執行順序。
05 這句程式碼在做什麼?為什麼要定義一個「空值」?
type GobCodec struct {
conn io.ReadWriteCloser
buf *bufio.Writer
dec *gob.Decoder
enc *gob.Encoder
}
type Codec interface {
io.Closer
ReadHeader(*Header) error
ReadBody(interface{}) error
Write(*Header, interface{}) error
}
var _ Codec = (*GobCodec)(nil)
作用:編譯期檢查 *GobCodec 是否完整實現了 Codec 介面。
- 把
nil轉成*GobCodec,再賦給Codec型變數。 - 若
*GobCodec沒有實現Codec的全部方法,這裡會編譯失敗。 var _ Codec = ...中的_表示不關心這個變數,只用來做型別檢查,不佔實際使用。
這是一種常見的「介面實現檢查」寫法,避免漏實現方法而在執行時才出錯 。
06 Go 的記憶體管理機制?(簡述)
Go 的記憶體管理大體參考 tcmalloc,本質上是一個記憶體池,並做了自動伸縮、合理切塊等優化。
基本概念
| 概念 | 說明 |
|---|---|
| Page | 一塊 8KB 的記憶體。Go 向作業系統申請/釋放記憶體多以「頁」為單位。 |
| span | 記憶體塊,由一個或多個連續的 page 組成。可視為一「小隊」的 page。 |
| sizeclass | 空間規格,標記該 span 的 page 如何被使用(存多大 object)。 |
| object | 存單一變數資料的記憶體單位。一個 span 會被切成多個等大的 object。例如 object 16B、span 8KB → 512 個 object。 |
三層結構
-
mheap
從作業系統要一大塊記憶體當記憶體池,並做整體管理:mheap.spans:存 page 與 span 資訊(起始位址、page 數、已用大小等)。mheap.bitmap:各 span 中物件的標記(如是否可回收)。mheap.arena_start:要分配給應用程式使用的區域起點。
-
mcentral
相同用途(相同 sizeclass)的 span 以鏈表形式放在 mcentral。分配時從這裡找合適的 span,取一個 object 回傳給上層。 -
mcache
每個處理器 P 對應一個 mcache,作為快取層,提高並發分配效率。申請記憶體時先從當前 P 的 mcache 分配,沒有可用 span 時再向 mcentral 要。
07 mutex 有幾種模式?
sync.Mutex 有兩種模式:normal 和 starvation。
正常模式(normal)
- 所有 goroutine 大體按 FIFO 排隊獲取鎖。
- 被喚醒的 goroutine 和 新來請求鎖的 goroutine 一起競爭,通常新來的更容易拿到鎖(持續佔用 CPU),被喚醒的較難拿到。
- 公平性:否,偏向新請求。
飢餓模式(starvation)
- 所有嘗試獲取鎖的 goroutine 都排隊等待。
- 新請求鎖的 goroutine 不會參與搶鎖(自旋被關閉),而是直接加入隊尾排隊。
- 公平性:是,避免某個 goroutine 長期拿不到鎖。
參考: