Go 探討了13年,怎麼解決再賦值的坑?

語言: CN / TW / HK

大家好,我是煎魚。

最近在看 Go 的一些歷史提案時,發現有個別很神奇的提案,已經提出來了許多年,但在如今依然沒有關閉,並且不斷地有人在討論,但又解決不了。

有種 “很氣又幹不掉我的樣子”,今天就由煎魚帶大家一起來看看是什麼。

背景

今天本文介紹的 Go 提案《proposal: spec: various changes to :=[1]》是經典中的經典,初學者學習時常犯的問題。

該提案自 2009 年提出,最近一次的激烈討論是 2021 年:

程式碼原型如下:

func f() (err os.Error) {
 v, err := g()
 if err != nil {
  return
 }
 if v {
  v, err := h()
  if err != nil {
   return
  }
 }
}

這段程式碼的問題在於函式片段中的 := 會導致產生一個新的 err 變數,該新變數會使得返回引數(err os.Error,也聲明瞭 err)被覆蓋。

也就是 Go 裡的 := 重新賦值的邏輯,會導致引數被覆蓋,從而引起隱藏問題,極其容易踩坑。

新提案

如開頭所說,這是一個經過了 13 年,在 2022 年依然沒有結局的提案。

煎魚總結了提案和其他討論的思路,社群一共提出瞭如下幾種解決方案或思路。如下:

  • 加語法糖。
  • 幹掉語法。
  • 定規範。

加語法糖

這個想法刪除了重新宣告 := 語法,並添加了新的 : 和 :: 語法,用於新變數宣告。

如下程式碼:

package bar

func foo() {
   var x, err = f()
   ...
   // 這裡 “:err” 表示上面宣告的 err。 
   var y, z, :err = g()
   ...
   {
    // 實際上,:err 表示程式碼區塊裡的已經宣告的 err。
    var w, :err = h()
    ...
    // ::err 表示包級別宣告的 err。
    var u, v, ::err = j()
    ...
    // 這個“err”是一個新的宣告。
    var m, n, err = k()
    ...
   }
}

上述程式碼中給出了三種案例,分別是:

  • var :err = x:表示最近一個作用域宣告的 err,原意是指上面一個宣告的 err,因此你會發現在程式碼區塊和外,代表著不同的結果。
  • var ::err = x:表示包級別宣告的 err。
  • var err = x:表示一個新的宣告。

幹掉語法

在另外一個提案《proposal: Go 2: let := support any l-value that = supports[2]》中 Go 語言之父 @ Rob Pike 直接表示想幹掉 := 這個重新賦值的方式,而不是再修修補補,加一堆會更復雜。

如下圖:

我認為我們應該以消除重新宣告為目標,如果我們能夠建立一個更平穩的錯誤處理模型,那麼重新宣告就變得不那麼引人注目了。不過這不會很快發生。

刪除功能而不是增加功能。

(大呼:less is more)

單行多次宣告

首先修改重新賦值的語義,:= 左邊的所有識別符號總是被宣告為新的變數,在同一個塊內重新宣告是不允許的。

如下程式碼:

a, err := foo()
b, err := foo() // 編譯錯誤,因為 var err 已在此塊中宣告

第一行宣告正常,第二行由於在同一個程式碼區塊重新聲明瞭,因此會出現編譯錯誤,因為已經宣告過了。

接著增加語法特性,允許在一行中混合使用 = 和 :=。如下程式碼:

// a 和 err 被宣告和初始化(相當於:a, err := foo()
a:=, err:= foo()

// b 被宣告和初始化,而 err 只被賦予了一個新值
b:=, err= foo()
if true {
    // c 在 if 塊中宣告並初始化,併為 err 分配一個新值
    c:=, err= foo()
}
if true {
    // d 和 err 在 if 塊中宣告,err 被隱藏
    d:=, err:= foo()
}

允許單行進行多次宣告,本質上是明確了宣告的範圍,會提高程式碼可讀性的複雜度。

總結

今天這篇文章給大家介紹了一個 13 年前(2009 年)就被發現的神坑。當初最早學習 Go 時,也碰到很多教程、文件,同學會遇到這個重新賦值宣告的神坑。

實際上上述的 3 個方案,看起來是從不同的角度補全了這個重新宣告的語法糖,但也加大了複雜度。

也許直接幹掉,也可能是個不錯的選擇?

參考資料

[1]proposal: spec: various changes to :=: http://github.com/golang/go/issues/377

[2]proposal: Go 2: let := support any l-value that = supports: http://github.com/golang/go/issues/30318