序言
筆者學習并使用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ù)的向用戶交付價值。