Golang初学者易犯的三种错误

序言

笔者学习并使用Golang已经有一个多月了,尽管Golang的特性少、语法简单且功能强大,但作为初学者,难免会犯一些大家都犯过的错误。笔者在实践的基础上,将初学者易犯的错误进行了简单梳理,暂时总结了三种错误,先分享给大家,希望对大家有一定的帮助。

资源关闭

这里的资源包括文件、数据库连接和Socket连接等,我们以文件操作为例,说明一下常见的资源关闭错误。

文件操作的一个代码示例:

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

一些同学写到这就开始专注业务代码了,最后“忘记”了写关闭文件操作的代码。殊不知,这里埋下了一个祸根。在Linux中,一切皆文件,当打开的文件数过多时,就会触发"too many open files“的系统错误,从而让整个系统陷入崩溃。

我们增加上关闭文件操作的代码,如下所示:

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

Golang提供了一个很好用的关键字defer,defer语句的含义是不管程序是否出现异常,均在函数退出时自动执行相关代码。遗憾的是,上面的修改又引入了新问题,即如果文件打开错误,调用file.Close会导致程序抛出异常(panic),所以正确的修改应该将file.Close放到错误检查之后,如下:

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

变量的大小写

Golang对关键字的增加非常吝啬,其中没有private、protected和public这样的关键字。要使某个符号对其他包(package)可见(即可以访问),需要将该符号定义为以大写字母开头,这些符号包括接口,类型,函数和变量等。

对于那些比较在意美感的程序员,尤其是工作在Linux平台上的C/C++程序员,函数名或变量名以大写字母开头可能会让他们感觉不太适应,同时他们严格遵循最小可见性的原则,接口名和类名以小写字母开头也会让他们很纠结。在他们自己写代码的时候可能会顺手将函数名或变量名改成以小写字母开头,当与小写字母开头的接口名或类型名冲突时(包内可见性),还得费心的另外想一个名字。如果不小心,将包外可见性的符号rename成了以小写字母开头,则会遇到编译错误,即明明有符号却偏偏找不到,不过这对于有一些编程经验的程序员来说还是比较好解决的。

下面的例子对于Golang的初学者,即使有一些编程经验,也较难排查,往往要花费稍微多一些的时间。

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)
}

我们看一下打印结果:

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}}]

很意外的是,我们反序列化后获取的对象数据是错误的,而json.Unmarshal没有返回任何异常。
为了进一步定位,我们将序列化后的json串打印出来:

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

从打印结果可以看出,Position的数据丢了,这使得我们想到了可见性,即大写的符号在包外可见。通过走查代码,我们发现Student的定义中,Position的变量名是小写开始的:

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

对于习惯写C/C++/Java代码的同学,修改这个变量的名字变得很纠结,以往“类名大写开头,对象名小写开头”的经验不再适用,不得不起一个不太顺溜的名字,比如缩写:

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

再次运行程序,结果正常,打印如下:

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感觉很刺眼,我们继续改进:

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字段在从结构体实例编码到JSON数据格式的时候,使用name作为名字,这可以看作是一种重命名的方式。

再次运行程序,结果是我们期望的,打印如下:

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编译器可以从初始化表达式的右值推导出该变量应该声明为哪种类型,这让Go语言看起来有点像动态类型语言,尽管Go语言实际上是不折不扣的强类型语言(静态类型语言)。

说明:感觉与C++11中auto关键字的作用有点类似

Golang中引入了一个关于错误处理的标准模式,即error接口,大家都太爱用了,以至于明显只有bool属性的返回值或变量都用error来修饰,我们看一个例子:

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

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

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

这里的两个局部变量err是同一个变量吗?答案是肯定的

通过冒号和等号的组合“:=”来进行变量初始化有一个限制,即出现在“:=”左侧的变量至少有一个是没有声明过的,否则编译失败。

很多人不知道这个规则,则写出下面的代码:

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

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

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

对于喜欢写简单优美代码的同学可能接受不了这样的命名,比如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,用foo函数的返回值对n进行赋值,在bar函数中使用n。
预期结果是bar()和main()中均输出5,但程序运行后的结果却不是我们期望的:

bar n: 0
main n: 5

通过增加打印进一步定位,发现main函数中调用foo函数后的n的地址(0x201d2210)与全局变量的n的地址(0x56b4a4)并不一样,也就是说前者是一个局部变量,同时从bar函数中的打印来看,全局变量n在foo函数返回时并未被赋值为它的返回值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)
}

再次运行该程序,执行结果完全符合预期:

bar n: 5
main n: 5

小结

本文总结了Golang初学者易犯的三种错误,包括资源关闭、符号的大小写和局部变量初始化,希望对像我一样的新手有一点帮助,从而在业务实现过程中少走一些弯路,更快更安全的面向业务编程,持续的向用户交付价值。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 前言 本规范是针对 Go 语言的编码规范,目的是为了统一项目的编码风格,提高源程序的可读性、可靠性和可重用性,从而...
    _张晓龙_阅读 1,884评论 5 21
  • 与爸爸关系好的女孩子会更漂亮。 在我的主观意识里,我就是这么认为的。 为什么呢? 生命当中,最初接触的异性就是爸爸...
    Azadzad阅读 5,941评论 0 1
  • 班级情况 校区:科学创想机器人茂业店 时间:周六下午3点30-5点30 学员:曲冠名,范凯博,郝嘉成 老师:张玲 ...
    乐搭阅读 464评论 0 1
  • 【一】 他热爱摄影,用心去捕捉一切美好。在他镜头里,山水、人物都自成一种韵味,有美在无声地流动。他为自己能定格那么...
    凌星虹阅读 438评论 0 27
  • 啤酒作为人类最古老的酒精饮料之一,于二十世纪初传入中国。 啤酒是以大麦芽﹑酒花﹑水为主要原料﹐经酵母发酵作用酿制而...
    咪咪盟阅读 718评论 0 1