你會犯這些 Go 編碼錯誤嗎(二)?

煎魚發表於2022-05-22

大家好,我是煎魚。

前一次給大家分享了《你會犯這些 Go 編碼錯誤嗎(一)?》,不知道大家吸收的怎麼樣,還有再踩到類似的坑嗎?

今天繼續來第二彈,跟煎魚上車。

Go 常見錯誤

6. 同名變數的作用域

問題

我們在編寫程式時,由於各種臨時變數,會常用變數名 n、i、err 等。有時候會遇到一些問題,如下程式碼:

func main() {
    n := 0
    if true {
        n := 1
        n++
    }
    fmt.Println(n)
}

程式的輸出結果是什麼。n 是 1,還是 2?

輸出結果:

0

解決方法

上述程式碼的 n := 1 又重新宣告瞭一個新變數,他是同名變數,同時很關鍵的,他包含同名區域性變數。

我們的 n++ 影響的是 if 區塊裡的變數 n,而不是外部的同名變數 n。如果要正確影響,應當修改為:

func main() {
    n := 0
    if true {
        n = 1
        n++
    }
    fmt.Println(n)
}

輸出結果:

2

這一個案例雖然單一提出來看並不複雜,但在許多 Go 初學者剛入門時經常會在應用程式中遇到類似的問題,然後問為什麼不行...

據魚的 7s 記憶,在全域性 DB 控制程式碼中等場景中遇到這個問題,來問煎魚的應該有 10 次以上,是一個比較高頻的 ”坑“ 了。

7. 迴圈中的臨時變數

問題

相信不少同學在業務程式碼中做過類似的事情,那就是:邊迴圈處理業務資料,邊變更值內容。

如下程式碼:

s := []int{1, 1, 1}
for _, n := range s {
    n += 1
}
fmt.Println(s)

程式輸出的結果是什麼,i 成功均都 +1 了嗎?

不,真正的輸出結果是:[1 1 1]

解決方法

實際上在迴圈中,我們所引用的變數是臨時變數,你去修改他是沒有任何意義的,修改的根本不是你的原資料的結構。

我們需要定位到原資料,根據索引去定位修改。如下程式碼:

s := []int{1, 1, 1}
for i := range s {
    s[i] += 1
}
fmt.Println(s)

輸出結果:

[2 2 2]

這很常見,一個不注意就會手抖。

8. JSON 轉換和輸出為空

問題

在做對外介面的資料對接和轉換時,我們經常需要對 JSON 資料進行處理。

如下程式碼:

type T struct {
    name string
    age  int
}

func main() {
    p := T{"煎魚", 18}
    jsonData, _ := json.Marshal(p)
    fmt.Println(string(jsonData))
}

輸出結果是什麼,能把姓名和年齡正常輸出嗎?

輸出結果:

{}

你沒有看錯,程式的輸出結果是空,沒有轉換到任何東西。

解決方法

原因是 JSON 的輸出,只會輸出公開(匯出)欄位,也就是首字母必須為大寫。

我們需要進行如下改造:

type T struct {
    Name string
    Age  int
}

func main() {
    p := T{"煎魚", 18}
    jsonData, _ := json.Marshal(p)
    ...
}

輸出結果:

{"Name":"煎魚","Age":18}

又或是顯式指定 JSON 的標籤:

type T struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

現在 IDE 能夠很方便的直接生成 JSON 標籤了,建議大家可以習慣性補上,確保欄位規範。

真的可以避免不少的對接時的拉扯和誤差。

9. 以為 recover 是萬能的

問題

在 Go 中,goroutine + panic + recover 是天作之合,用起來很方便。常常會有同學以為他是萬能的。

如下程式碼:

func gohead() {
    go func() {
        panic("煎魚下班了")
    }()
}

func main() {
    go func() {
        defer func() {
            if r := recover(); r != nil {
                fmt.Println(r)
            }
        }()

        gohead()
    }()

    time.Sleep(time.Second)
}

你認為輸出結果是什麼。程式被中斷,還是成功 recover 了?

輸出結果:

panic: 煎魚下班了

goroutine 17 [running]:
main.gohead.func1()
        /Users/eddycjy/awesomeProject/main.go:10 +0x39
created by main.gohead
        /Users/eddycjy/awesomeProject/main.go:9 +0x35

你以為 recover 是萬能的?並不。

解決方法

Go1 現階段沒有萬能解決方法,只能遵守 Go 的規範(Go 就是不能跨 goroutine recover,是正確的邏輯)。

Goroutine 建議每個 goroutine 都需要有 recover 兜底,否則一旦出現 panic 就會導致應用中斷,容器會重啟。

另外 Go 底層主動丟擲的致命錯誤 throw,是沒有辦法使用 recover 攔截的,請務必注意。

注:有同學一直認為 recover 可以攔截到 throw,特此說明。

10. nil 不是 nil

問題

我們在做程式的邏輯處理時,經常要對介面(interface)值進行判斷。

如下程式碼:

func Foo() error {
    var err *os.PathError = nil
    return err
}

func main() {
    err := Foo()
    fmt.Println(err)
    fmt.Println(err == nil)
}

你認為輸出結果是什麼,是 nil 和 true 嗎?

輸出結果:

<nil>
false

表面看起來的 nil ,它並不等於 nil。

解決方法

介面值,是特殊的。只有當它的值和動態型別都為 nil 時,介面值才等於 nil。

在前面的問題程式碼中,實際上函式 Foo 返回的是 [nil, *os.PathError],我們將其與 [nil, nil] 進行比較,所以是 false。

如果要準確判斷,要進行如下轉換:

fmt.Println(err == (*os.PathError)(nil))

輸出結果就會為 true。

另外就是儘量使用 error 型別,又或是避免與 interface 進行比較,這是比較危險的行為(有不少人不知道這一現象)。

總結

在今天這篇文章中,我們開始了 Go 常見編碼錯誤的第二節,共涉及 5 個案例:

  1. 同名變數的作用域。
  2. 迴圈中的臨時變數。
  3. JSON 轉換和輸出為空。
  4. 以為 recover 是萬能的。
  5. nil 不是 nil。

這依然是非常常見的案例,你有沒有遇到過?或是有其它的新的案例呢?

歡迎大家一起交流和溝通。

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

推薦閱讀

參考

相關文章