Skip to main content

Go 面試題二(記憶體、並發、OOP)

來源:Durant Thorvalds


01 newmake 的區別?

  • 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 實現面向對象的兩個關鍵是 structinterface

封裝

  • 同一個 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

若需要負數怎麼做?

  1. 改用有符號型別(最直接):需要可能為負的差值時,用 int / int32 / int64

    var a, b int = 1, 2
    fmt.Println(a - b) // -1
  2. 先轉成有符號再相減:資料本身是 uint 時,先轉成夠大的有符號型別再算,並注意溢出(例如 int 裝不下 uint 最大值時用 int64 或先檢查範圍)。

    var a, b uint = 1, 2
    diff := int(a) - int(b)
    fmt.Println(diff) // -1
  3. 需要「帶正負的差值」且要防溢出:先判斷誰大再決定正負,或使用 int64 並在轉換前檢查是否在安全範圍內。

    var a, b uint64 = 1, 2
    var diff int64
    if 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 沒有明確定義,寫程式時不要依賴其順序。
  • 不同 packageinit 依「包導入的依賴關係」決定執行順序。

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 要。

參考:Go 語言記憶體管理(二):Go 記憶體管理


07 mutex 有幾種模式?

sync.Mutex 有兩種模式:normalstarvation

正常模式(normal)

  • 所有 goroutine 大體按 FIFO 排隊獲取鎖。
  • 被喚醒的 goroutine新來請求鎖的 goroutine 一起競爭,通常新來的更容易拿到鎖(持續佔用 CPU),被喚醒的較難拿到。
  • 公平性:否,偏向新請求。

飢餓模式(starvation)

  • 所有嘗試獲取鎖的 goroutine 都排隊等待。
  • 新請求鎖的 goroutine 不會參與搶鎖(自旋被關閉),而是直接加入隊尾排隊。
  • 公平性:是,避免某個 goroutine 長期拿不到鎖。

參考: