从golang小白到成为golang工程师快两个月了,我要分享一下新手在开发中常犯的错误,都是我亲自踩过的坑。这些错误中有些会导致无法通过编译,这种错容易发现,而有些错误在编译时不会抛出,甚至在运行时也不会panic,如果缺少相关的知识,挠破头皮都搞不清楚bug出在哪。
1.对nil map、nil slice 添加数据
请考虑一下这段代码是否有错,然后运行一遍:
package main func main() { var m map[string]string m["name"] = "zzy"}
不出意外的话,这段代码将导致一个panic:
panic: assignment to entry in nil map
这是因为代码中只是声明了map的类型,却没有为map创建底层数组,此时的map实际上在内存中还不存在,即nil map,可以运行下面的代码进行验证:
package mainimport "fmt"func main() { var m map[string]string if m == nil { fmt.Println("this map is a nil map") } }
所以想要顺利的使用map,一定要使用内建函数make函数进行创建:
m := make(map[string]string)
使用字面量的方式也是可以的,效果同make:
m := map[string]string{}
同样的,直接对nil slice添加数据也是不允许的,因为slice的底层也是数组,没有经过make函数初始化时,只是声明了slice类型,而底层数组是不存在的:
package main func main() { var s []int s[0] = 1}
上面的代码将产生一个panicruntime error:index out of range
,正确做法应该是使用make函数或者字面量:
package main func main() { //第二个参数是slice的len,make slice时必须提供,还可以传入第三个参数作为cap s := make([]int, 1) s[0] = 1}
可能有人发现对nil slice使用append函数而不经过make也是有效的:
package mainimport "fmt"func main() { var s []int s = append(s, 1) fmt.Println(s) // s => [1]}
那是因为slice本身其实类似一个struct,它有一个len属性,是当前长度,还有个cap属性,是底层数组的长度,append函数会判断传入的slice的len和cap,如果len即将大于cap,会调用make函数生成一个更大的新数组并将原底层数组的数据复制过来(以上均为本人猜测,未经查证,有兴趣的同学可以去挑战一下源码),过程类似:
package mainimport "fmt"func main() { var s []int //len(s)和cap(s)都是0 s = append(s, 1) fmt.Println(s) // s => [1]}func append(s []int, arg int) []int { newLen := len(s) + 1 var newS []int if newLen > cap(s) { //创建新的slice,其底层数组扩容为原先的两倍多 newS = make([]int, newLen, newLen*2) copy(newS, s) } else { newS = s[:newLen] //直接在原数组上切一下就行 } newS[len(s)] = arg return newS }
对nil map、nil slice的错误使用并不是很可怕,毕竟编译的时候就能发觉,下面要说的一个错误则非常坑爹,一不小心中招的话,很难排查。
2.误用:=赋值导致变量覆盖
先看下这段代码,猜猜会打印出什么:
package mainimport ( "errors" "fmt")func main() { i := 2 if i > 1 { i, err := doDivision(i, 2) if err != nil { panic(err) } fmt.Println(i) } fmt.Println(i) }func doDivision(x, y int) (int, error) { if y == 0 { return 0, errors.New("input is invalid") } return x / y, nil }
我估计有人会认为是:
1 1
实际执行一遍,结果是:
1 2
为什么会这样呢!?
这是因为golang中变量的作用域范围小到每个词法块(不理解的同学可以简单的当成{}
包裹的部分)都是一个单独的作用域,大家都知道每个作用域的内部声明会屏蔽外部同名的声明,而每个if
语句都是一个词法块,也就是说,如果在某个if
语句中,不小心用:=
而不是=
对某个if
语句外的变量进行赋值,那么将产生一个新的局部变量,并仅仅在if
语句中的这个赋值语句后有效,同名的外部变量会被屏蔽,将不会因为这个赋值语句之后的逻辑产生任何变化!
在语言层面这也许并不是个错误,但是实际工作中如果误用,那么产生的bug会很隐秘。比如例子中的代码,因为err
是之前未声明的,所以使用了:=
赋值(图省事,少写了var err error
),然后既不会在编译时报错,也不会在运行时报错,它会让你百思不得其解,觉得自己的逻辑明明走对了,为什么最后的结果却总是不对,直到你一点一点调试,才发现自己不小心多写了一个:
。
我因为这个被坑过好几回了,每次都查了好久,以为是自己逻辑有漏洞,最后发现是把=
写成了:=
,唉,说起来都是泪。
3.将值传递当成引用传递
值类型数据和引用类型数据的区别我相信在座的各位都能分得清,否则不用往下看了,因为看不懂。
在golang中,array
和struct
都是值类型的,而slice
、map
、chan
是引用类型,所以我们写代码的时候,基本不使用array
,而是用slice
代替它,对于struct
则尽量使用指针,这样避免传递变量时复制数据的时间和空间消耗,也避免了无法修改原数据的情况。
如果对这点认识不清,导致的后果可能是代码有瑕疵,更严重的是产生bug。
考虑这段代码并运行一下:
package mainimport "fmt"type person struct { name string age byte isDead bool}func main() { p1 := person{name: "zzy", age: 100} p2 := person{name: "dj", age: 99} p3 := person{name: "px", age: 20} people := []person{p1, p2, p3} whoIsDead(people) for _, p := range people { if p.isDead { fmt.Println("who is dead?", p.name) } } } func whoIsDead(people []person) { for _, p := range people { if p.age < 50 { p.isDead = true } } }
我相信很多人一看就看出问题在哪了,但肯定还有人不清楚for range
语法的机制,我絮叨一下:golang中for range
语法非常方便,可以轻松的遍历array
、slice
、map
等结构,但是它有一个特点,就是会在遍历时把当前遍历到的元素,复制给内部变量,具体就是在whoIsDead
函数中的for range
里,会把people
里的每个person
,都复制给p
这个变量,类似于这样的操作:
p := person
上文说过,struct
是值类型,所以在赋值给p
的过程中,实际上需要重新生成一份person
数据,便于for range
内部使用,不信试试:
package mainimport "fmt"type person struct { name string age byte isDead bool}func main() { p1 := person{name: "zzy", age: 100} p2 := p1 p1.name = "changed" fmt.Println(p2.name) }
所以p.isDead = true
这个操作实际上更改的是新生成的p
数据,而非people
中原本的person
,这里产生了一个bug。
在for range
内部只需读取数据而不需要修改的情况下,随便怎么写也无所谓,顶多就是代码不够完美,而需要修改数据时,则最好传递struct
指针:
package mainimport "fmt"type person struct { name string age byte isDead bool}func main() { p1 := &person{name: "zzy", age: 100} p2 := &person{name: "dj", age: 99} p3 := &person{name: "px", age: 20} people := []*person{p1, p2, p3} whoIsDead(people) for _, p := range people { if p.isDead { fmt.Println("who is dead?", p.name) } } } func whoIsDead(people []*person) { for _, p := range people { if p.age < 50 { p.isDead = true } } }
作者:柔顺的灵魂
链接:https://www.jianshu.com/p/42954b8b4ccc