17

golang新手容易犯的三个错误

 5 years ago
source link: https://studygolang.com/articles/14144?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

从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 main

import "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
}

上面的代码将产生一个panic runtime 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 main

import "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 main

import "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 main

import (
    "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
}

我估计有人会认为是:

实际执行一遍,结果是:

为什么会这样呢!?

这是因为golang中变量的作用域范围小到每个词法块(不理解的同学可以简单的当成 {} 包裹的部分)都是一个单独的作用域,大家都知道每个作用域的内部声明会屏蔽外部同名的声明,而每个 if 语句都是一个词法块,也就是说,如果在某个 if 语句中,不小心用 := 而不是 = 对某个 if 语句外的变量进行赋值,那么将产生一个新的局部变量,并仅仅在 if 语句中的这个赋值语句后有效,同名的外部变量会被屏蔽,将不会因为这个赋值语句之后的逻辑产生任何变化!

在语言层面这也许并不是个错误,但是实际工作中如果误用,那么产生的bug会很隐秘。比如例子中的代码,因为 err 是之前未声明的,所以使用了 := 赋值(图省事,少写了 var err error ),然后既不会在编译时报错,也不会在运行时报错,它会让你百思不得其解,觉得自己的逻辑明明走对了,为什么最后的结果却总是不对,直到你一点一点调试,才发现自己不小心多写了一个

我因为这个被坑过好几回了,每次都查了好久,以为是自己逻辑有漏洞,最后发现是把 = 写成了 := ,唉,说起来都是泪。

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

值类型数据和引用类型数据的区别我相信在座的各位都能分得清,否则不用往下看了,因为看不懂。

在golang中, arraystruct 都是值类型的,而 slicemapchan 是引用类型,所以我们写代码的时候,基本不使用 array ,而是用 slice 代替它,对于 struct 则尽量使用指针,这样避免传递变量时复制数据的时间和空间消耗,也避免了无法修改原数据的情况。

如果对这点认识不清,导致的后果可能是代码有瑕疵,更严重的是产生bug。

考虑这段代码并运行一下:

package main

import "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 语法非常方便,可以轻松的遍历 arrayslicemap 等结构,但是它有一个特点,就是会在遍历时把当前遍历到的元素,复制给内部变量,具体就是在 whoIsDead 函数中的 for range 里,会把 people 里的每个 person ,都复制给 p 这个变量,类似于这样的操作:

p := person

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

package main

import "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 main

import "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
        }
    }
}

运行一下:

who is dead? px

everything is ok,很棒棒的代码。

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

func whoIsDead(people []person) {
    for i := 0; i < len(people); i++ {
        if people[i].age < 50 {
            people[i].isDead = true
        }
    }
}

好, for range 部分讲到这里,接下来说一说 map 结构中值的传递和修改问题。

这段代码将之前的 people []person 改成了 map 结构,大家觉得有错误吗,如果有错,错在哪:

package main

import "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 := map[string]person{
        p1.name: p1,
        p2.name: p2,
        p3.name: p3,
    }
    whoIsDead(people)
    if p3.isDead {
        fmt.Println("who is dead?", p3.name)
    }
}

func whoIsDead(people map[string]person) {
    for name, _ := range people {
        if people[name].age < 50 {
            people[name].isDead = true
        }
    }
}

go run 一下,报错:

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

这个报错有点迷,我估计很多人都看不懂了。我解答下, map 底层使用了 array 存储数据,并且没有容量限制,随着 map 元素的增多,需要创建更大的 array 来存储数据,那么之前的地址就无效了,因为数据被复制到了新的更大的 array 中,所以 map 中元素是不可取址的,也是不可修改的。这个报错的意思其实就是不允许修改 map 中的元素。

即便 map 中元素没有以上限制,这段代码依然是错误的,想一想,为什么?答案之前已经说过了。

那么,怎么改才能正确呢,老套路,依然是使用指针:

package main

import "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 := map[string]*person{
        p1.name: p1,
        p2.name: p2,
        p3.name: p3,
    }
    whoIsDead(people)
    if p3.isDead {
        fmt.Println("who is dead?", p3.name)
    }
}

func whoIsDead(people map[string]*person) {
    for name, _ := range people {
        if people[name].age < 50 {
            people[name].isDead = true
        }
    }
}

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

package main

type person struct {
    name   string
    age    byte
    isDead bool
}

func main() {
    p := person{name: "zzy", age: 100}
    isDead(p)
}

func isDead(p interface{}) {
    if p.(person).age < 101 {
        p.(person).isDead = true
    }
}

会直接报一个编译错误:

cannot assign to p.(person).isDead

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

package main

import "fmt"

type person struct {
    name   string
    age    byte
    isDead bool
}

func main() {
    p := &person{name: "zzy", age: 100}
    isDead(p)
    fmt.Println(p)
}

func isDead(p interface{}) {
    if p.(*person).age < 101 {
        p.(*person).isDead = true
    }
}

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK