剛開始進入這個行業時,常聽到Senior Engineer和Junior Engineer的區別之一,就是資深工程師懂得多,擁有更多解決問題的方式。然而,隨著時間的推移,我逐漸了解這背後的意思——所有技術決策其實都是一種取捨(trade-off),因此我們必須從全局來考量。而要能做到這點的基礎,就是你懂得足夠多。
工欲善其事,必先利其器。
上一章提到 Golang 的設計,雖然它不像其他語言有那麼多花哨的功能,但擁有自動垃圾回收(GC)機制。然而,這並不是每個人都喜歡的功能。Discord 就因為 Go 的垃圾回收問題,轉而選擇 Rust。
這不是說 Go 的垃圾回收設計不好,而是它不再符合他們的使用需求。
第一步:了解使用需求,來決定使用哪種語言
在選擇語言時,首要的工作是明確專案的需求,根據這些需求來決定最適合的語言。
第二步:了解各語言的限制,避免踩雷
接下來的重點,就是理解每種語言的限制,找出哪些情況會導致語言無法滿足需求,進而做出最佳選擇。
slice 和 map 沒有初始化或指定大小,是初學 Go 時很容易犯的錯誤。
Slice 的問題
我們來看看,當 slice 沒有指定大小時,持續 append 新元素會有什麼影響。
package main
import "fmt"
func main() {
var mySlice []int // 未初始化,初始值nil
fmt.Printf("初始化:長度 = %d, 容量 = %d, 資料 = %v\n", len(mySlice), cap(mySlice), mySlice)
// 持續 append 新元素
for i := 1; i <= 10; i++ {
mySlice = append(mySlice, i)
fmt.Printf("第 %d 次 append:長度 = %d, 容量 = %d, 資料 = %v\n", i, len(mySlice), cap(mySlice), mySlice)
}
}
輸出結果:
初始化:長度 = 0, 容量 = 0, 資料 = []
第 1 次 append:長度 = 1, 容量 = 1, 資料 = [1]
第 2 次 append:長度 = 2, 容量 = 2, 資料 = [1, 2]
第 3 次 append:長度 = 3, 容量 = 4, 資料 = [1, 2, 3]
第 4 次 append:長度 = 4, 容量 = 4, 資料 = [1, 2, 3, 4]
第 5 次 append:長度 = 5, 容量 = 8, 資料 = [1, 2, 3, 4, 5]
第 6 次 append:長度 = 6, 容量 = 8, 資料 = [1, 2, 3, 4, 5, 6]
第 7 次 append:長度 = 7, 容量 = 8, 資料 = [1, 2, 3, 4, 5, 6, 7]
第 8 次 append:長度 = 8, 容量 = 8, 資料 = [1, 2, 3, 4, 5, 6, 7, 8]
第 9 次 append:長度 = 9, 容量 = 16, 資料 = [1, 2, 3, 4, 5, 6, 7, 8, 9]
第 10 次 append:長度 = 10, 容量 = 16, 資料 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
append 的作用機制:
重新分配內存的影響:
因此,千萬別犯這種錯誤,否則很容易暴露自己還不是 Senior。
map的問題
map沒有指定初始大小時,可能會造成更大的問題
尤其是當 map 不斷擴展時。原因在於 map 的底層結構是由多個 "bucket" 組成,這些桶用來存放鍵值對。
當你向 map 添加越來越多的鍵值對時,Go 會動態分配更多的桶來容納新數據,但這些桶一旦被分配,是不會縮減的,即使你刪除了一些鍵值對,map 的大小不會因此縮減。
為什麼這樣會更糟?
最佳解法:
當下能做的,千萬別拖到最後才做。
一開始就指定大小:對於 slice 和 map,能夠預估的初始大小最好一開始就設定好,這樣可以避免頻繁的記憶體擴展,提升性能。
定期清理和重建 slice 和 map:如果經常處理大量數據或刪除操作,定期重建 slice 或 map 可以幫助釋放多餘的內存。特別是 slice,如果涉及到創建子 slice,需要特別注意,因為底層記憶體可能依然被占用。最好的方法是使用 copy 函數將數據複製到一個新的 slice 中。 slice細節瞭解可以到這看看
千萬別等到系統變慢、性能下降,才開始懷疑人生,懷疑code
有沒有例外的時候,不要一開始就分配大小的時候
有的
這樣做,nil slice就不會被分配記憶體
func getData(condition bool) []int {
if !condition {
return nil //return nil slice
}
return []int{1, 2, 3}
}
defer的問題
func readFile() {
f, _ := os.Open("file.txt")
// 忘記使用 defer f.Close()
}
這個真的就是超級低級錯誤
但我還真的看過資深工程師做過
不管是哪種程式語言,正確地關閉和釋放資源都是良好的編碼習慣。這包括但不限於:
魔鬼都在細節裡
小事做好了,大事自然成
引用資料來源:https://www.manning.com/books/100-go-mistakes-and-how-to-avoid-them