序言
筆者學(xué)習(xí)并使用Golang已經(jīng)有一個(gè)多月了看成,盡管Golang的特性少君编、語法簡(jiǎn)單且功能強(qiáng)大,但作為初學(xué)者绍昂,難免會(huì)犯一些大家都犯過的錯(cuò)誤啦粹。筆者在實(shí)踐的基礎(chǔ)上,將初學(xué)者易犯的錯(cuò)誤進(jìn)行了簡(jiǎn)單梳理窘游,暫時(shí)總結(jié)了三種錯(cuò)誤唠椭,先分享給大家,希望對(duì)大家有一定的幫助忍饰。
資源關(guān)閉
這里的資源包括文件贪嫂、數(shù)據(jù)庫(kù)連接和Socket連接等,我們以文件操作為例艾蓝,說明一下常見的資源關(guān)閉錯(cuò)誤力崇。
文件操作的一個(gè)代碼示例:
file, err := os.Open("test.go")
if err != nil {
fmt.Println("open file failed:", err)
return
}
...
一些同學(xué)寫到這就開始專注業(yè)務(wù)代碼了,最后“忘記”了寫關(guān)閉文件操作的代碼赢织。殊不知亮靴,這里埋下了一個(gè)禍根。在Linux中于置,一切皆文件茧吊,當(dāng)打開的文件數(shù)過多時(shí),就會(huì)觸發(fā)"too many open files“的系統(tǒng)錯(cuò)誤,從而讓整個(gè)系統(tǒng)陷入崩潰搓侄。
我們?cè)黾由详P(guān)閉文件操作的代碼瞄桨,如下所示:
file, err := os.Open("test.go")
defer file.Close()
if err != nil {
fmt.Println("open file failed:", err)
return
}
...
Golang提供了一個(gè)很好用的關(guān)鍵字defer,defer語句的含義是不管程序是否出現(xiàn)異常讶踪,均在函數(shù)退出時(shí)自動(dòng)執(zhí)行相關(guān)代碼芯侥。遺憾的是,上面的修改又引入了新問題乳讥,即如果文件打開錯(cuò)誤柱查,調(diào)用file.Close會(huì)導(dǎo)致程序拋出異常(panic),所以正確的修改應(yīng)該將file.Close放到錯(cuò)誤檢查之后雏婶,如下:
file, err := os.Open("test.go")
if err != nil {
fmt.Println("open file failed:", err)
return
}
defer file.Close()
...
變量的大小寫
Golang對(duì)關(guān)鍵字的增加非常吝嗇物赶,其中沒有private白指、protected和public這樣的關(guān)鍵字留晚。要使某個(gè)符號(hào)對(duì)其他包(package)可見(即可以訪問),需要將該符號(hào)定義為以大寫字母開頭告嘲,這些符號(hào)包括接口错维,類型,函數(shù)和變量等橄唬。
對(duì)于那些比較在意美感的程序員赋焕,尤其是工作在Linux平臺(tái)上的C/C++程序員,函數(shù)名或變量名以大寫字母開頭可能會(huì)讓他們感覺不太適應(yīng)仰楚,同時(shí)他們嚴(yán)格遵循最小可見性的原則隆判,接口名和類名以小寫字母開頭也會(huì)讓他們很糾結(jié)。在他們自己寫代碼的時(shí)候可能會(huì)順手將函數(shù)名或變量名改成以小寫字母開頭僧界,當(dāng)與小寫字母開頭的接口名或類型名沖突時(shí)(包內(nèi)可見性)侨嘀,還得費(fèi)心的另外想一個(gè)名字。如果不小心捂襟,將包外可見性的符號(hào)rename成了以小寫字母開頭咬腕,則會(huì)遇到編譯錯(cuò)誤,即明明有符號(hào)卻偏偏找不到葬荷,不過這對(duì)于有一些編程經(jīng)驗(yàn)的程序員來說還是比較好解決的涨共。
下面的例子對(duì)于Golang的初學(xué)者,即使有一些編程經(jīng)驗(yàn)宠漩,也較難排查举反,往往要花費(fèi)稍微多一些的時(shí)間。
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}}]
很意外的是扒吁,我們反序列化后獲取的對(duì)象數(shù)據(jù)是錯(cuò)誤的火鼻,而json.Unmarshal沒有返回任何異常。
為了進(jìn)一步定位,我們將序列化后的json串打印出來:
Serialize:data is : [{"Name":"zhangsan","Sex":"male","Age":20},{"Name":"lisi","Sex":"female","Age":18}]
從打印結(jié)果可以看出凝危,Position的數(shù)據(jù)丟了波俄,這使得我們想到了可見性,即大寫的符號(hào)在包外可見蛾默。通過走查代碼懦铺,我們發(fā)現(xiàn)Student的定義中,Position的變量名是小寫開始的:
type Student struct {
Name string
Sex string
Age int
position Position
}
對(duì)于習(xí)慣寫C/C++/Java代碼的同學(xué)支鸡,修改這個(gè)變量的名字變得很糾結(jié)冬念,以往“類名大寫開頭,對(duì)象名小寫開頭”的經(jīng)驗(yàn)不再適用牧挣,不得不起一個(gè)不太順溜的名字急前,比如縮寫:
type Student struct {
Name string
Sex string
Age int
Posi Position
}
再次運(yùn)行程序,結(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}}]
對(duì)于json串裆针,很多人喜歡全小寫,對(duì)于大寫開頭的key感覺很刺眼寺晌,我們繼續(xù)改進(jìn):
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"`
}
兩個(gè)斜點(diǎn)之間的代碼世吨,比如json:"name"
,作用是Name字段在從結(jié)構(gòu)體實(shí)例編碼到JSON數(shù)據(jù)格式的時(shí)候呻征,使用name作為名字耘婚,這可以看作是一種重命名的方式。
再次運(yùn)行程序陆赋,結(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中有一種局部變量初始化方法,即使用冒號(hào)和等號(hào)的組合“:=”來進(jìn)行變量聲明和初始化,這使得我們?cè)谑褂镁植孔兞繒r(shí)很方便攒岛。
初始化一個(gè)局部變量的代碼可以這樣寫:
v := 10
指定類型已不再是必需的赖临,Go編譯器可以從初始化表達(dá)式的右值推導(dǎo)出該變量應(yīng)該聲明為哪種類型,這讓Go語言看起來有點(diǎn)像動(dòng)態(tài)類型語言阵子,盡管Go語言實(shí)際上是不折不扣的強(qiáng)類型語言(靜態(tài)類型語言)思杯。
說明:感覺與C++11中auto關(guān)鍵字的作用有點(diǎn)類似
Golang中引入了一個(gè)關(guān)于錯(cuò)誤處理的標(biāo)準(zhǔn)模式,即error接口挠进,大家都太愛用了色乾,以至于明顯只有bool屬性的返回值或變量都用error來修飾,我們看一個(gè)例子:
port, err := createPort()
if err != nil {
return
}
veth, err := createVeth()
if err != nil {
return
}
err = insert()
if err != nil {
return
}
...
這里的兩個(gè)局部變量err是同一個(gè)變量嗎领突?答案是肯定的
通過冒號(hào)和等號(hào)的組合“:=”來進(jìn)行變量初始化有一個(gè)限制暖璧,即出現(xiàn)在“:=”左側(cè)的變量至少有一個(gè)是沒有聲明過的,否則編譯失敗君旦。
很多人不知道這個(gè)規(guī)則澎办,則寫出下面的代碼:
port, errPort := createPort()
if errPort != nil {
return
}
veth, errVeth := createVeth()
if errVeth != nil {
return
}
errInsert := insert()
if errInsert != nil {
return
}
...
對(duì)于喜歡寫簡(jiǎn)單優(yōu)美代碼的同學(xué)可能接受不了這樣的命名嘲碱,比如errPort, errVeth和errInsert等,所以對(duì)于error接口的變量命名局蚀,在筆者心中的baby names只有一個(gè)麦锯,那就是err。
除過命名琅绅,另一個(gè)常見錯(cuò)誤是局部變量有可能遮蓋或隱藏全局變量扶欣,因?yàn)橥ㄟ^“:=”方式初始化的局部變量看不到全局變量。
我們先看一段代碼:
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)
}
這段代碼的原意是定義一個(gè)包內(nèi)的全局變量n千扶,用foo函數(shù)的返回值對(duì)n進(jìn)行賦值料祠,在bar函數(shù)中使用n。
預(yù)期結(jié)果是bar()和main()中均輸出5澎羞,但程序運(yùn)行后的結(jié)果卻不是我們期望的:
bar n: 0
main n: 5
通過增加打印進(jìn)一步定位髓绽,發(fā)現(xiàn)main函數(shù)中調(diào)用foo函數(shù)后的n的地址(0x201d2210)與全局變量的n的地址(0x56b4a4)并不一樣,也就是說前者是一個(gè)局部變量妆绞,同時(shí)從bar函數(shù)中的打印來看顺呕,全局變量n在foo函數(shù)返回時(shí)并未被賦值為它的返回值5,仍然是初始的默認(rèn)值0摆碉。
最初對(duì)語句“n, err := foo()”的理解是塘匣,Golang會(huì)定義新變量err,n為初始定義的那個(gè)全局變量巷帝。但實(shí)際情況是,對(duì)于使用“:=”定義的變量扫夜,如果新變量n與那個(gè)已同名定義的變量(這里就是那個(gè)全局變量n)不在一個(gè)作用域中時(shí)楞泼,那么Golang會(huì)新定義這個(gè)變量n,并遮蓋或隱藏住大作用域的同名變量笤闯,這就是導(dǎo)致該問題的真兇堕阔。
知道真兇后就很好解決了,即我們用“=”代替“:=":
func main() {
var err error
n, err = foo()
if err != nil {
fmt.Println(err)
return
}
bar()
fmt.Println("main n:", n)
}
再次運(yùn)行該程序颗味,執(zhí)行結(jié)果完全符合預(yù)期:
bar n: 5
main n: 5
小結(jié)
本文總結(jié)了Golang初學(xué)者易犯的三種錯(cuò)誤超陆,包括資源關(guān)閉、符號(hào)的大小寫和局部變量初始化浦马,希望對(duì)像我一樣的新手有一點(diǎn)幫助时呀,從而在業(yè)務(wù)實(shí)現(xiàn)過程中少走一些彎路,更快更安全的面向業(yè)務(wù)編程晶默,持續(xù)的向用戶交付價(jià)值谨娜。