為什麼 Go 用起來會難受?這 6 個細節你知道嗎

煎魚發表於2022-05-09

大家好,我是煎魚。

在做新的應用選型時,我們會進行應用程式語言的選擇,這時會糾結 Java、PHP、Go...各種,會思考有沒有致命的問題,不能用?

可以明確的是,Go 沒有非常致命的問題,否則你我他都不會在這裡相遇,也不會大火。

難受的點,倒是有不少,今天就由煎魚和大家一起來看看。

難受的點

泛型

在 Go1.18 以前,在所有社交媒體和調查報告上來看。Go 最難受的莫過於沒有泛型,

寫一個通用的方法,要不得把入參宣告為 interface,要不得寫 N 個不同型別的一樣程式碼的函式,程式碼重複率高。

如下圖:

這是 Go1.18 以前最難受的點,現在新版本雖然有了泛型,但現階段配套標準和開源庫還沒完全到位,影響還是會繼續存在。

淺拷貝和洩露

在寫 Go 程式時,我們經常要用到 slice、map 等基礎型別。但有一個比較麻煩的點,就是會涉及到淺拷貝。

一個不注意就會引起 BUG,如下程式碼:

type T struct {
    A string
    B []string
}

func main() {
    x := T{"煎魚", []string{"上班"}}

    y := x
    y.A = "鹹魚"
    y.B[0] = "下班"

    fmt.Println(x)
    fmt.Println(y)
}

輸出結果是什麼?

煎魚到底是上班了,還是下班了?

結果如下:

{煎魚 [下班]}
{鹹魚 [下班]}

實際上在 y := x 時,他拷貝的是指向物件的指標,這個時候 xy 的底層資料其實是一家子,自然一變動 yx 的煎魚也就下班了。

同型別的 slice 也有 append 的洩露,以及 len、cap 的不準確問題,是比較折騰人的。

洩露的示例:

var a []int

func f(b []int) []int {
 a = b[:2]
 return a
}

func main() {
    ...
}

有興趣的可以具體看《Go 切片導致記憶體洩露,被坑兩次了!》的解析。

錯誤處理

在 Go 的錯誤處理中,許多小夥伴的難受的點分兩大塊。一個是大量重複的 if err != nil 的程式碼:

func main() {
 x, err := foo()
 if err != nil {
   // handle error
 }
 y, err := foo()
 if err != nil {
   // handle error
 }
 z, err := foo()
 if err != nil {
   // handle error
 }
 s, err := foo()
 if err != nil {
   // handle error
 }
}

另外一塊是在異常處理中,Go 現階段是 panic 和 recover 的模式,內部還包含 throw 的致命性錯誤丟擲,無法攔截,為此我也見過個別事故是因此造成的。

這是一個爭議性很大的板塊。

nil 介面不是 nil

我們強行將一段 Go 程式的變數值賦為 nil,並進行 nil 與 nil 的判斷。

程式碼如下:

func main() {
    var v interface{}
    v = (*int)(nil)
    fmt.Println(v == nil)
}

輸出的結果是什麼。是 false,還是 true,又或是丟擲異常?

輸出結果是 fasle,nil 可不 100% 等於 nil。

這與 interface 的內部資料結構有關,是在程式設計時要注意的一個細節,具體可詳見《Go 面試題:Go interface 的一個 “坑” 及原理分析》的解析。

垃圾回收

Go 非常簡潔,垃圾回收唯一的可調節的是 GC 頻率,可以通過 GOGC 變數設定初始垃圾收集器的目標百分比值。

$ GOGC=100 eddycjy

簡單來講就是,GOGC 的值設定的越大,GC 的頻率越低,但每次最終所觸發到 GC 的堆記憶體也會更大。

然後就沒別的方式可以優化垃圾回收器本身了,以至於當年我還被人拿 Java 來吐槽過一遍,說 Go 肯定有。

依賴管理

壓軸的難受點,莫過於 Go 的依賴管理。先是從 GOPATH 時代,開源後一路水土不服,後面 rsc 直接下場支稜起來硬推。

到 2022 年,目前 Go modules 還是會存在一些讓人難受的點。甚至曹大總結了 《Go mod 七宗罪》,不少我在工作中也遇到和替別人解決過,非常的精闢。

引用如下 7 點:

  • Go 命令的副作用:所有 go build、go list、go test 多多少少都會拉取到牆外的資源,會很慢。
  • 形同虛設的 semver 規範:go mod 的設計,就是希望大家在軟體庫釋出時都要遵守標準,例如在小版本時,要保持相容性。但這非常理想化,現實就是經常有人不遵守。
  • 無法應對刪庫:釋出後的軟體庫,你已經拉取了。但釋出者依然可以刪除,受傷的還是自己。
  • goproxy 的實現並不統一:作者 A、作者 B、作者 C 寫的幾套 goproxy 內部邏輯是不完全一致的,很折騰人。
  • go get 到的 lib 版本在 go build 時被修改。
  • 版本資訊擴散:匯入路徑是包含版本號 v1、v2 等資訊的,一旦修改,就得大面積替換。
  • go.sum 合併衝突:大型專案上的多人維護,導致頻繁衝突。

熟悉掌握 Go 的一個表現,那就是精通 Go modules,不然專案都執行的不順利。

總結

今天我們圍繞 Go 的難受場景進行了分析和講解,本文涉及的分別是:泛型、淺拷貝和洩露、錯誤處理、nil 介面不是 nil、垃圾回收、依賴管理。

其中不少是常見的,也有的是有意而為之(例如:垃圾回收)。從大家的角度來看,你覺得 Go 比較難受的點還有哪些呢?

歡迎大家在評論區一起留言和交流。

文章持續更新,可以微信搜【腦子進煎魚了】閱讀,本文 GitHub github.com/eddycjy/blog 已收錄,學習 Go 語言可以看 Go 學習地圖和路線,歡迎 Star 催更。

推薦閱讀

參考

相關文章