序言
笔者学习并使用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初学者易犯的三种错误,包括资源关闭、符号的大小写和局部变量初始化,希望对像我一样的新手有一点帮助,从而在业务实现过程中少走一些弯路,更快更安全的面向业务编程,持续的向用户交付价值。
有疑问加站长微信联系(非本文作者)