大家好,我是煎魚。
前一次給大家分享了《你會犯這些 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 個案例:
- 同名變數的作用域。
- 迴圈中的臨時變數。
- JSON 轉換和輸出為空。
- 以為 recover 是萬能的。
- nil 不是 nil。
這依然是非常常見的案例,你有沒有遇到過?或是有其它的新的案例呢?
歡迎大家一起交流和溝通。
文章持續更新,可以微信搜【腦子進煎魚了】閱讀,本文 GitHub github.com/eddycjy/blog 已收錄,學習 Go 語言可以看 Go 學習地圖和路線,歡迎 Star 催更。