golang新手容易犯的3个错误

Posted Golang语言社区

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了golang新手容易犯的3个错误相关的知识,希望对你有一定的参考价值。

从golang小白到成为golang工程师快两个月了,我要分享一下新手在开发中常犯的错误,都是我亲自踩过的坑。这些错误中有些会导致无法通过编译,这种错容易发现,而有些错误在编译时不会抛出,甚至在运行时也不会panic,如果缺少相关的知识,挠破头皮都搞不清楚bug出在哪。


1.对nil map、nil slice 添加数据

请考虑一下这段代码是否有错,然后运行一遍:

1package main
2
3func main() {
4    var m map[string]string
5    m["name"] = "zzy"
6}


不出意外的话,这段代码将导致一个panic:


1panic: assignment to entry in nil map


这是因为代码中只是声明了map的类型,却没有为map创建底层数组,此时的map实际上在内存中还不存在,即nil map,可以运行下面的代码进行验证:


 1package main
2
3import "fmt"
4
5func main() {
6    var m map[string]string
7    if m == nil {
8        fmt.Println("this map is a nil map")
9    }
10}


所以想要顺利的使用map,一定要使用内建函数make函数进行创建:


1m := make(map[string]string)


使用字面量的方式也是可以的,效果同make:


1m := map[string]string{}


同样的,直接对nil slice添加数据也是不允许的,因为slice的底层也是数组,没有经过make函数初始化时,只是声明了slice类型,而底层数组是不存在的:


1package main
2
3func main() {
4    var s []int
5    s[0] = 1
6}


上面的代码将产生一个panicruntime error:index out of range,正确做法应该是使用make函数或者字面量:


1package main
2
3func main() {
4    //第二个参数是slice的len,make slice时必须提供,还可以传入第三个参数作为cap  
5    s := make([]int1
6    s[0] = 1
7}


可能有人发现对nil slice使用append函数而不经过make也是有效的:


1package main
2
3import "fmt"
4
5func main() {
6    var s []int
7    s = append(s, 1)
8    fmt.Println(s) // s => [1]
9}


那是因为slice本身其实类似一个struct,它有一个len属性,是当前长度,还有个cap属性,是底层数组的长度,append函数会判断传入的slice的len和cap,如果len即将大于cap,会调用make函数生成一个更大的新数组并将原底层数组的数据复制过来(以上均为本人猜测,未经查证,有兴趣的同学可以去挑战一下源码),过程类似:


 1package main
2
3import "fmt"
4
5func main() {
6    var s []int //len(s)和cap(s)都是0
7    s = append(s, 1)
8    fmt.Println(s) // s => [1]
9}
10
11func append(s []int, arg int) []int {
12    newLen := len(s) + 1
13    var newS []int
14    if newLen > cap(s) {
15        //创建新的slice,其底层数组扩容为原先的两倍多
16        newS = make([]int, newLen, newLen*2)
17        copy(newS, s)
18    } else {
19        newS = s[:newLen] //直接在原数组上切一下就行
20    }
21    newS[len(s)] = arg
22    return newS
23}


对nil map、nil slice的错误使用并不是很可怕,毕竟编译的时候就能发觉,下面要说的一个错误则非常坑爹,一不小心中招的话,很难排查。


2.误用:=赋值导致变量覆盖

先看下这段代码,猜猜会打印出什么:


 1package main
2
3import (
4    "errors"
5    "fmt"
6)
7
8func main() {
9    i := 2
10    if i > 1 {
11        i, err := doDivision(i, 2)
12        if err != nil {
13            panic(err)
14        }
15        fmt.Println(i)
16    }
17    fmt.Println(i)
18}
19
20func doDivision(x, y int) (int, error) {
21    if y == 0 {
22        return 0, errors.New("input is invalid")
23    }
24    return x / y, nil
25}


我估计有人会认为是:

1 1

实际执行一遍,结果是:

1 2

为什么会这样呢!?
这是因为golang中变量的作用域范围小到每个词法块(不理解的同学可以简单的当成
{}  包裹的部分)都是一个单独的作用域,大家都知道每个作用域的内部声明会屏蔽外部同名的声明,而每个if语句都是一个词法块,也就是说,如果在某个if语句中,不小心用:=而不是=对某个if语句外的变量进行赋值,那么将产生一个新的局部变量,并仅仅在if语句中的这个赋值语句后有效,同名的外部变量会被屏蔽,将不会因为这个赋值语句之后的逻辑产生任何变化!
在语言层面这也许并不是个错误,但是实际工作中如果误用,那么产生的bug会很隐秘。比如例子中的代码,因为
err是之前未声明的,所以使用了:=赋值(图省事,少写了var err error),然后既不会在编译时报错,也不会在运行时报错,它会让你百思不得其解,觉得自己的逻辑明明走对了,为什么最后的结果却总是不对,直到你一点一点调试,才发现自己不小心多写了一个
我因为这个被坑过好几回了,每次都查了好久,以为是自己逻辑有漏洞,最后发现是把
=写成了:=,唉,说起来都是泪。


3.将值传递当成引用传递

值类型数据和引用类型数据的区别我相信在座的各位都能分得清,否则不用往下看了,因为看不懂。
在golang中,
arraystruct都是值类型的,而slicemapchan是引用类型,所以我们写代码的时候,基本不使用array,而是用slice代替它,对于struct则尽量使用指针,这样避免传递变量时复制数据的时间和空间消耗,也避免了无法修改原数据的情况。
如果对这点认识不清,导致的后果可能是代码有瑕疵,更严重的是产生bug。
考虑这段代码并运行一下:


 1package main
2
3import "fmt"
4
5type person struct {
6    name   string
7    age    byte
8    isDead bool
9}
10
11func main() {
12    p1 := person{name: "zzy", age: 100}
13    p2 := person{name: "dj", age: 99}
14    p3 := person{name: "px", age: 20}
15    people := []person{p1, p2, p3}
16    whoIsDead(people)
17    for _, p := range people {
18        if p.isDead {
19            fmt.Println("who is dead?", p.name)
20        }
21    }
22}
23
24func whoIsDead(people []person) {
25    for _, p := range people {
26        if p.age < 50 {
27            p.isDead = true
28        }
29    }
30}


我相信很多人一看就看出问题在哪了,但肯定还有人不清楚for range语法的机制,我絮叨一下:golang中for range语法非常方便,可以轻松的遍历arrayslicemap等结构,但是它有一个特点,就是会在遍历时把当前遍历到的元素,复制给内部变量,具体就是在whoIsDead函数中的for range里,会把people里的每个person,都复制给p这个变量,类似于这样的操作:


1p := person


上文说过,struct是值类型,所以在赋值给p的过程中,实际上需要重新生成一份person数据,便于for range内部使用,不信试试:


 1package main
2
3import "fmt"
4
5type person struct {
6    name   string
7    age    byte
8    isDead bool
9}
10
11func main() {
12    p1 := person{name: "zzy", age: 100}
13    p2 := p1
14    p1.name = "changed"
15    fmt.Println(p2.name)
16}


所以p.isDead = true这个操作实际上更改的是新生成的p数据,而非people中原本的person,这里产生了一个bug。
for range内部只需读取数据而不需要修改的情况下,随便怎么写也无所谓,顶多就是代码不够完美,而需要修改数据时,则最好传递struct指针:


 1package main
2
3import "fmt"
4
5type person struct {
6    name   string
7    age    byte
8    isDead bool
9}
10
11func main() {
12    p1 := &person{name: "zzy", age: 100}
13    p2 := &person{name: "dj", age: 99}
14    p3 := &person{name: "px", age: 20}
15    people := []*person{p1, p2, p3}
16    whoIsDead(people)
17    for _, p := range people {
18        if p.isDead {
19            fmt.Println("who is dead?", p.name)
20        }
21    }
22}
23
24func whoIsDead(people []*person) {
25    for _, p := range people {
26        if p.age < 50 {
27            p.isDead = true
28        }
29    }
30}


运行一下:


who is dead? px


everything is ok,很棒棒的代码。
还有另外的方法,使用索引访问
people中的person,改动一下whoIsDead函数,也能达到同样的目的:


1func whoIsDead(people []person) {
2    for i := 0; i < len(people); i++ {
3        if people[i].age < 50 {
4            people[i].isDead = true
5        }
6    }
7}


好,for range部分讲到这里,接下来说一说map结构中值的传递和修改问题。
这段代码将之前的
people []person改成了map结构,大家觉得有错误吗,如果有错,错在哪:


 1package main
2
3import "fmt"
4
5type person struct {
6    name   string
7    age    byte
8    isDead bool
9}
10
11func main() {
12    p1 := person{name: "zzy", age: 100}
13    p2 := person{name: "dj", age: 99}
14    p3 := person{name: "px", age: 20}
15    people := map[string]person{
16        p1.name: p1,
17        p2.name: p2,
18        p3.name: p3,
19    }
20    whoIsDead(people)
21    if p3.isDead {
22        fmt.Println("who is dead?", p3.name)
23    }
24}
25
26func whoIsDead(people map[string]person) {
27    for name, _ := range people {
28        if people[name].age < 50 {
29            people[name].isDead = true
30        }
31    }
32}


go run一下,报错:


1cannot assign to struct field people[name].isDead in map



 1package main
2
3import "fmt"
4
5type person struct {
6    name   string
7    age    byte
8    isDead bool
9}
10
11func main() {
12    p1 := &person{name: "zzy", age: 100}
13    p2 := &person{name: "dj", age: 99}
14    p3 := &person{name: "px", age: 20}
15    people := map[string]*person{
16        p1.name: p1,
17        p2.name: p2,
18        p3.name: p3,
19    }
20    whoIsDead(people)
21    if p3.isDead {
22        fmt.Println("who is dead?", p3.name)
23    }
24}
25
26func whoIsDead(people map[string]*person) {
27    for name, _ := range people {
28        if people[name].age < 50 {
29            people[name].isDead = true
30        }
31    }
32}


另外,在interface{}断言里试图直接修改struct属性而非通过指针修改时:


 1package main
2
3type person struct {
4    name   string
5    age    byte
6    isDead bool
7}
8
9func main() {
10    p := person{name: "zzy", age: 100}
11    isDead(p)
12}
13
14func isDead(p interface{}) {
15    if p.(person).age < 101 {
16        p.(person).isDead = true
17    }
18}


会直接报一个编译错误:


1cannot assign to p.(person).isDead


即便编译通过,代码也是错误的 ,始终要记住struct是值类型的数据,请使用指针去操作它, 正确做法是:


 1package main
2
3import "fmt"
4
5type person struct {
6    name   string
7    age    byte
8    isDead bool
9}
10
11func main() {
12    p := &person{name: "zzy", age: 100}
13    isDead(p)
14    fmt.Println(p)
15}
16
17func isDead(p interface{}) {
18    if p.(*person).age < 101 {
19        p.(*person).isDead = true
20    }
21}


最后,不能不说golang中指针真是居家旅行、升职加薪的必备知识啊,希望同学们熟练掌握。


版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。



Golang语言社区

ID:Golangweb

www.bytedancing.com

游戏服务器架构丨分布式技术丨大数据丨游戏算法学习


以上是关于golang新手容易犯的3个错误的主要内容,如果未能解决你的问题,请参考以下文章

开发新手最容易犯的50个 Ruby on Rails 错误

开发新手最容易犯的50个 Ruby on Rails 错误

C++编程新手容易犯的10种编程错误

Python老手也会犯的20个新手级错误

C++编程新手容易犯的 10 种编程错误

PHP程序员最容易犯的Mysql错误