Golang初學者易犯的三種錯誤

序言

筆者學習并使用Golang已經(jīng)有一個多月了,盡管Golang的特性少、語法簡單且功能強大,但作為初學者,難免會犯一些大家都犯過的錯誤。筆者在實踐的基礎(chǔ)上,將初學者易犯的錯誤進行了簡單梳理,暫時總結(jié)了三種錯誤,先分享給大家,希望對大家有一定的幫助。

資源關(guān)閉

這里的資源包括文件、數(shù)據(jù)庫連接和Socket連接等,我們以文件操作為例,說明一下常見的資源關(guān)閉錯誤。

文件操作的一個代碼示例:

file, err := os.Open("test.go") 
if err != nil {
    fmt.Println("open file failed:", err)
    return
}
...

一些同學寫到這就開始專注業(yè)務(wù)代碼了,最后“忘記”了寫關(guān)閉文件操作的代碼。殊不知,這里埋下了一個禍根。在Linux中,一切皆文件,當打開的文件數(shù)過多時,就會觸發(fā)"too many open files“的系統(tǒng)錯誤,從而讓整個系統(tǒng)陷入崩潰。

我們增加上關(guān)閉文件操作的代碼,如下所示:

file, err := os.Open("test.go")
defer file.Close()
if err != nil {
    fmt.Println("open file failed:", err)
    return
}
...

Golang提供了一個很好用的關(guān)鍵字defer,defer語句的含義是不管程序是否出現(xiàn)異常,均在函數(shù)退出時自動執(zhí)行相關(guān)代碼。遺憾的是,上面的修改又引入了新問題,即如果文件打開錯誤,調(diào)用file.Close會導致程序拋出異常(panic),所以正確的修改應(yīng)該將file.Close放到錯誤檢查之后,如下:

file, err := os.Open("test.go")
if err != nil {
    fmt.Println("open file failed:", err)
    return
}
defer file.Close()
...

變量的大小寫

Golang對關(guān)鍵字的增加非常吝嗇,其中沒有private、protected和public這樣的關(guān)鍵字。要使某個符號對其他包(package)可見(即可以訪問),需要將該符號定義為以大寫字母開頭,這些符號包括接口,類型,函數(shù)和變量等。

對于那些比較在意美感的程序員,尤其是工作在Linux平臺上的C/C++程序員,函數(shù)名或變量名以大寫字母開頭可能會讓他們感覺不太適應(yīng),同時他們嚴格遵循最小可見性的原則,接口名和類名以小寫字母開頭也會讓他們很糾結(jié)。在他們自己寫代碼的時候可能會順手將函數(shù)名或變量名改成以小寫字母開頭,當與小寫字母開頭的接口名或類型名沖突時(包內(nèi)可見性),還得費心的另外想一個名字。如果不小心,將包外可見性的符號rename成了以小寫字母開頭,則會遇到編譯錯誤,即明明有符號卻偏偏找不到,不過這對于有一些編程經(jīng)驗的程序員來說還是比較好解決的。

下面的例子對于Golang的初學者,即使有一些編程經(jīng)驗,也較難排查,往往要花費稍微多一些的時間。

type Position struct {
    X int 
    Y int
    Z int
}

type Student struct {
    Name string
    Sex string
    Age int
    position Position
}

func main(){
    position1 := Position{10, 20, 30}
    student1 := Student{"zhangsan", "male", 20, position1}
    position2 := Position{15, 10, 20}
    student2 := Student{"lisi", "female", 18, position2}    

    var srcSlice = make([]Student, 2)
    srcSlice[0] = student1
    srcSlice[1] = student2
    fmt.Printf("Init:srcSlice is : %v\n", srcSlice)
    data, err := json.Marshal(srcSlice)
    if err != nil{
        fmt.Printf("Serialize:json.Marshal error! %v\n", err)
        return
    }

    var dstSliece = make([]Student, 2)
    err = json.Unmarshal(data, &dstSliece)
    if err != nil {
        fmt.Printf("Deserialize: json.Unmarshal error! %v\n", err)
        return
    }
    fmt.Printf("Deserialize:dstSlice is : %v\n", dstSliece)
}

我們看一下打印結(jié)果:

Init:srcSlice is : [{zhangsan male 20 {10 20 30}} {lisi female 18 {15 10 20}}]
Deserialize:dstSliece is : [{zhangsan male 20 {0 0 0}} {lisi female 18 {0 0 0}}]

很意外的是,我們反序列化后獲取的對象數(shù)據(jù)是錯誤的,而json.Unmarshal沒有返回任何異常。
為了進一步定位,我們將序列化后的json串打印出來:

Serialize:data is : [{"Name":"zhangsan","Sex":"male","Age":20},{"Name":"lisi","Sex":"female","Age":18}]

從打印結(jié)果可以看出,Position的數(shù)據(jù)丟了,這使得我們想到了可見性,即大寫的符號在包外可見。通過走查代碼,我們發(fā)現(xiàn)Student的定義中,Position的變量名是小寫開始的:

type Student struct {
    Name string
    Sex string
    Age int
    position Position
}

對于習慣寫C/C++/Java代碼的同學,修改這個變量的名字變得很糾結(jié),以往“類名大寫開頭,對象名小寫開頭”的經(jīng)驗不再適用,不得不起一個不太順溜的名字,比如縮寫:

type Student struct {
    Name string
    Sex string
    Age int
    Posi Position
}

再次運行程序,結(jié)果正常,打印如下:

Init:srcSlice is : [{zhangsan male 20 {10 20 30}} {lisi female 18 {15 10 20}}]
Serialize:data is : [{"Name":"zhangsan","Sex":"male","Age":20,"Posi":{"X":10,"Y":20,"Z":30}},{"Name":"lisi","Sex":"female","Age":18,"Posi":{"X":15,"Y":10,"Z":20}}]
Deserialize:dstSliece is : [{zhangsan male 20 {10 20 30}} {lisi female 18 {15 10 20}}]

對于json串,很多人喜歡全小寫,對于大寫開頭的key感覺很刺眼,我們繼續(xù)改進:

type Position struct {
    X int `json:"x"`
    Y int `json:"y"`
    Z int `json:"z"`
}

type Student struct {
    Name string `json:"name"`
    Sex string `json:"sex"`
    Age int `json:"age"`
    Posi Position `json:"position"`
}

兩個斜點之間的代碼,比如json:"name",作用是Name字段在從結(jié)構(gòu)體實例編碼到JSON數(shù)據(jù)格式的時候,使用name作為名字,這可以看作是一種重命名的方式。

再次運行程序,結(jié)果是我們期望的,打印如下:

Init:srcSlice is : [{zhangsan male 20 {10 20 30}} {lisi female 18 {15 10 20}}]
Serialize:data is : [{"name":"zhangsan","sex":"male","age":20,"position":{"x":10,"y":20,"z":30}},{"name":"lisi","sex":"female","age":18,"position":{"x":15,"y":10,"z":20}}]
Deserialize:dstSliece is : [{zhangsan male 20 {10 20 30}} {lisi female 18 {15 10 20}}]

局部變量初始化(:=)

Golang中有一種局部變量初始化方法,即使用冒號和等號的組合“:=”來進行變量聲明和初始化,這使得我們在使用局部變量時很方便。

初始化一個局部變量的代碼可以這樣寫:

v := 10

指定類型已不再是必需的,Go編譯器可以從初始化表達式的右值推導出該變量應(yīng)該聲明為哪種類型,這讓Go語言看起來有點像動態(tài)類型語言,盡管Go語言實際上是不折不扣的強類型語言(靜態(tài)類型語言)。

說明:感覺與C++11中auto關(guān)鍵字的作用有點類似

Golang中引入了一個關(guān)于錯誤處理的標準模式,即error接口,大家都太愛用了,以至于明顯只有bool屬性的返回值或變量都用error來修飾,我們看一個例子:

port, err := createPort()
if err != nil {
    return
}

veth, err := createVeth()
if err != nil {
    return
}

err = insert()
if err != nil {
    return
}
...

這里的兩個局部變量err是同一個變量嗎?答案是肯定的

通過冒號和等號的組合“:=”來進行變量初始化有一個限制,即出現(xiàn)在“:=”左側(cè)的變量至少有一個是沒有聲明過的,否則編譯失敗。

很多人不知道這個規(guī)則,則寫出下面的代碼:

port, errPort := createPort()
if errPort != nil {
    return
}

veth, errVeth := createVeth()
if errVeth != nil {
    return
}

errInsert := insert()
if errInsert != nil {
    return
}
...

對于喜歡寫簡單優(yōu)美代碼的同學可能接受不了這樣的命名,比如errPort, errVeth和errInsert等,所以對于error接口的變量命名,在筆者心中的baby names只有一個,那就是err。

除過命名,另一個常見錯誤是局部變量有可能遮蓋或隱藏全局變量,因為通過“:=”方式初始化的局部變量看不到全局變量。

我們先看一段代碼:

var n int

func foo() (int, error) {
    return 5, nil
}

func bar() {
    fmt.Println("bar n:", n) 
}

func main() {
    n, err := foo()
    if err != nil {
        fmt.Println(err)
        return
    }
    bar()
    fmt.Println("main n:", n)
}

這段代碼的原意是定義一個包內(nèi)的全局變量n,用foo函數(shù)的返回值對n進行賦值,在bar函數(shù)中使用n。
預(yù)期結(jié)果是bar()和main()中均輸出5,但程序運行后的結(jié)果卻不是我們期望的:

bar n: 0
main n: 5

通過增加打印進一步定位,發(fā)現(xiàn)main函數(shù)中調(diào)用foo函數(shù)后的n的地址(0x201d2210)與全局變量的n的地址(0x56b4a4)并不一樣,也就是說前者是一個局部變量,同時從bar函數(shù)中的打印來看,全局變量n在foo函數(shù)返回時并未被賦值為它的返回值5,仍然是初始的默認值0。

最初對語句“n, err := foo()”的理解是,Golang會定義新變量err,n為初始定義的那個全局變量。但實際情況是,對于使用“:=”定義的變量,如果新變量n與那個已同名定義的變量(這里就是那個全局變量n)不在一個作用域中時,那么Golang會新定義這個變量n,并遮蓋或隱藏住大作用域的同名變量,這就是導致該問題的真兇。

知道真兇后就很好解決了,即我們用“=”代替“:=":

func main() {
    var err error
    n, err = foo()
    if err != nil {
        fmt.Println(err)
        return
    }
    bar()
    fmt.Println("main n:", n)
}

再次運行該程序,執(zhí)行結(jié)果完全符合預(yù)期:

bar n: 5
main n: 5

小結(jié)

本文總結(jié)了Golang初學者易犯的三種錯誤,包括資源關(guān)閉、符號的大小寫和局部變量初始化,希望對像我一樣的新手有一點幫助,從而在業(yè)務(wù)實現(xiàn)過程中少走一些彎路,更快更安全的面向業(yè)務(wù)編程,持續(xù)的向用戶交付價值。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 前言 本規(guī)范是針對 Go 語言的編碼規(guī)范,目的是為了統(tǒng)一項目的編碼風格,提高源程序的可讀性、可靠性和可重用性,從而...
    _張曉龍_閱讀 1,981評論 5 21
  • 與爸爸關(guān)系好的女孩子會更漂亮。 在我的主觀意識里,我就是這么認為的。 為什么呢? 生命當中,最初接觸的異性就是爸爸...
    Azadzad閱讀 6,088評論 0 1
  • 班級情況 校區(qū):科學創(chuàng)想機器人茂業(yè)店 時間:周六下午3點30-5點30 學員:曲冠名,范凱博,郝嘉成 老師:張玲 ...
    樂搭閱讀 496評論 0 1
  • 【一】 他熱愛攝影,用心去捕捉一切美好。在他鏡頭里,山水、人物都自成一種韻味,有美在無聲地流動。他為自己能定格那么...
    凌星虹閱讀 554評論 0 32
  • 啤酒作為人類最古老的酒精飲料之一,于二十世紀初傳入中國。 啤酒是以大麥芽﹑酒花﹑水為主要原料﹐經(jīng)酵母發(fā)酵作用釀制而...
    咪咪盟閱讀 786評論 0 1