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, 2
fmt.Println(a - b) // -1 -
先轉成有符號再相減:資料本身是
uint時,先轉成夠大的有符號型別再算,並注意溢出(例如int裝不下uint最大值時用int64或先檢查範圍)。var a, b uint = 1, 2
diff := int(a) - int(b)
fmt.Println(diff) // -1 -
需要「帶正負的差值」且要防溢出:先判斷誰大再決定正負,或使用
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 沒有明確定義,寫程式時不要依賴其順序。 - 不同 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 = ...中的_表示不關心這個變數,只用來做型別檢查,不佔實際使用。
這是一種常見的「介面實現檢查」寫法,避免漏實現方法而在執行時才出錯。