Go 面試中的隱藏陷阱:SliceHeader 問題解析
大家好,我是煎魚。
最近也是面試季+畢業季了,很多同學正在積極準備面試。尤其是很多同學,已經通過官網資料熟悉了 Go 基本語法,但沒有太大把握。希望對一些常見的棘手面試問題做一些預習。
今天和大家學習 @Harutyun Mardirossian 大佬分享的面試題,一起進步!
面試問題
請先在腦子里思考一下具體的運行結果,再查看答案。
如下代碼:
func main() {
s := make([]int, 0, 2)
doSomething(s)
fmt.Println(s)
}
func doSomething(a []int) {
a = append(a, 1)
}
面試問題:fmt.Println 的輸出結果是什么?
問題解析
運行程序,查看輸出結果:
[]
fmt.Println 最終打印的是一個長度為 0 的切片。
答案是:空切片。(你答對了嗎?)
在 Go 中,函數參數是按值傳遞的,這意味著上述代碼在參數傳遞時,創建了參數值的副本并傳遞給函數。
而切片實際上是一個包含長度(len)、容量(cap)和指向底層數組指針(data)的結構體。
當我們將切片作為函數參數傳遞時,實質上復制的是切片的 SliceHeader,對應的底層數組是保持不變的。
結合代碼來講,就是因為在 doSomething 函數中,創建了 SliceHeader 的新副本。然后 append 函數會在超過容量時重新分配新切片,并返回更新后的切片。
深入驗證
我們可以使用 unsafe 包去打印 SliceHeader(切片頭),進行進一步的驗證和分析。
如下代碼:
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
func main() {
s := make([]int, 0, 2)
sh := (*SliceHeader)(unsafe.Pointer(&s))
fmt.Println(sh)
doSomething(s)
}
func doSomething(a []int) {
a = append(a, 1)
sh := (*SliceHeader)(unsafe.Pointer(&a))
fmt.Println(sh)
}
輸出結果:
&{1374389592336 0 2} // main
&{1374389592336 1 2} // doSomething
兩個切片的 Data 指針地址指向的是同一個底層數組。但由于長度不同,它們在應用的表現上是兩個不同的切片。
這也印證了前面問題的結果是輸出了空切片,切片長度為 0 的內部原理。
變通方法
這種情況下,建議是修改寫法,提高代碼易讀性。否則后續維護也比較麻煩,不熟悉的同學咋一眼一看很有可能發現不了問題。
但如果你還是希望輸出你想要的切片值,可以采取以下變通方法。
改動后的代碼:
func main() {
s := make([]int, 0, 2)
doSomething(s)
fmt.Println(s[:1]) // 進行新的切片操作
}
func doSomething(a []int) {
a = append(a, 1)
}
輸出結果:
[1]
原因是在進行 s[:1] 切片操作時,本質上是創建了一個新的 SliceHeader,所以可以正常打印和獲取預期的元素。
當然,還有一種常見的寫法就是切片 append 等變更后一定做一遍再賦值,這樣可以規避掉不少使用上的細節坑。
總結
今天這篇文章討論了一個很常見的 Go 面試問題,內容涉及切片作為函數參數的傳遞和修改。
重點在于切片作為參數是按值傳遞的,因此函數內部的修改不會影響外部變量。
如果仍然希望獲取可以通過切片操作,重新切分一下新的切片結果集就可以了。