• 企业400电话
  • 微网小程序
  • AI电话机器人
  • 电商代运营
  • 全 部 栏 目

    企业400电话 网络优化推广 AI电话机器人 呼叫中心 网站建设 商标✡知产 微网小程序 电商运营 彩铃•短信 增值拓展业务
    golang新手们容易犯的3个错误总结

    前言

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

    我估计有人会认为是:

    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 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 语法非常方便,可以轻松的遍历 array 、 slice 、 map 等结构,但是它有一个特点,就是会在遍历时把当前遍历到的元素,复制给内部变量,具体就是在 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中指针真是居家旅行、升职加薪的必备知识啊,希望同学们熟练掌握。

    总结

    以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对脚本之家的支持。

    您可能感兴趣的文章:
    • Golang报“import cycle not allowed”错误的2种解决方法
    • Golang常见错误之值拷贝和for循环中的单一变量详解
    • Golang巧用defer进行错误处理的方法
    • golang log4go的日志输出优化详解
    • Golang中重复错误处理的优化方法
    上一篇:详解Go 语言中的比较操作符
    下一篇:Golang报“import cycle not allowed”错误的2种解决方法
  • 相关文章
  • 

    © 2016-2020 巨人网络通讯 版权所有

    《增值电信业务经营许可证》 苏ICP备15040257号-8

    golang新手们容易犯的3个错误总结 golang,新手,们容,易犯,的,