关于golang:这些常见的-Go-编码错误你犯过吗一

46次阅读

共计 3120 个字符,预计需要花费 8 分钟才能阅读完成。

大家好,我是煎鱼。

在用 Go 编程时,总会遇到各种奇奇怪怪的谬误,国内外曾经有许多小伙伴总结过(参考链接见参考),感觉都能凑一桌了。

之前始终想写,想着五一假期肝一肝。明天给大家分享 Go 里常见的编码谬误(一),心愿对大家有所帮忙。

Go 常见谬误

1. nil Map

问题

在程序中申明(定义)了一个 map,而后间接写入数据。如下代码:

func main() {var m map[string]string
    m["煎鱼"] = "进脑子了"
}

输入后果:

panic: assignment to entry in nil map

会间接抛出一个 panic。

解决办法

解决办法其实就是要申明并初始化,Go 里规范写法是调用 make 函数就能够了。如下代码:

func main() {m := make(map[string]string)
    m["煎鱼"] = "上班了"
}

这个问题在初学 Go 时是最容易踩到的谬误。

2. 空指针的援用

问题

咱们在 Go 常常会利用构造体去申明一系列的办法,他看起来向面向对象中的”类“,在业务代码中十分常见。

如下代码:

type Point struct {X, Y float64}

func (p *Point) Abs() float64 {return math.Sqrt(p.X*p.X + p.Y*p.Y)
}

func main() {
    var p *Point
    fmt.Println(p.Abs())
}

这段程序可能失常运行吗?失常计算和输入?

输入后果:

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x10a3143]

goroutine 1 [running]:
main.(*Point).Abs(...)
        /Users/eddycjy/awesomeProject/main.go:13
main.main()
        /Users/eddycj/awesomeProject/main.go:18 +0x23

间接就恐慌了,因为空指针的援用。

解决办法

如果变量 p 是一个指针,则必须要进行初始化才能够进行调用。如下代码:

func main() {var p *Point = new(Point)
    fmt.Println(p.Abs())
}

又或是用值对象的办法来解决:

func main() {var p Point // has zero value Point{X:0, Y:0}
    fmt.Println(p.Abs())
}

3. 应用对循环迭代器变量的援用

问题

在 Go 中,循环迭代器变量是一个繁多的变量,在每个循环迭代中取不同的值。这如果使用不当,可能会导致非预期的行为。

如下代码:

func main() {var out []*int
    for i := 0; i < 3; i++ {out = append(out, &i)
    }
    fmt.Println("Values:", *out[0], *out[1], *out[2])
    fmt.Println("Addresses:", out[0], out[1], out[2])
}

输入后果是什么。大胆猜测值是 1,2,3,地址都是不一样的。对吗?

输入后果:

Values: 3 3 3
Addresses: 0x40e020 0x40e020 0x40e020

值都是 3,地址都是同一个指向。

解决办法

其中一种解决办法是将循环变量复制到一个新变量中:

 for i := 0; i < 3; i++ {
     i := i // Copy i into a new variable.
     out = append(out, &i)
 }

输入后果:

Values: 0 1 2
Addresses: 0x40e020 0x40e024 0x40e028

起因是:在每次迭代中,咱们将 i 的地址追加到 out 切片中,但因为它是同一个变量,咱们实际上追加的是雷同的地址,该地址最终蕴含调配给 i 的最初一个值。

所以只须要拷贝一份,让两者脱离关联就能够了。

4. 在循环迭代器变量上应用 goroutine

问题

在 Go 中进行循环时,咱们常常会应用 goroutine 来并发解决数据。最经典的就是会联合闭包来编写业务逻辑。

如下代码:

values := []int{1, 2, 3, 4, 5}
for _, val := range values {go func() {fmt.Println(val)
    }()}

time.Sleep(time.Second)

但在理论的运行中,上述 for 循环可能无奈达到您的预期,你想的可能是程序输入切片中的值。

输入的后果是:

5
5
4
5
5

你可能会看到每次迭代打印的最初一个元素,甚至你会发现,每次输入的后果还不一样 …

如果去掉休眠代码,会发现 goroutine 可能基本不会开始执行,程序就完结了。

解决办法

这其实就是闭包应用上的一个常见问题,编写该闭包循环的正确办法是:

values := []int{1, 2, 3, 4, 5}
    for _, val := range values {go func(val int) {fmt.Println(val)
        }(val)
    }

通过将 val 作为参数增加到闭包中,在每次循环时,变量 val 都会被存储在 goroutine 的堆栈中,以确保最终 goroutine 执行时值是对的。

当然,这里还有一个隐性问题。大家总会认为是按程序输入 1, 2, 3, 4, 5。其实不然,因为 goroutine 的执行是具备随机性的,没法确保程序。

注:常常会变形呈现在许多 Go 的面试题当中,一旦简单起来就容易让人蛊惑。

5. 数组不会被扭转

问题

切片和数字是咱们在 Go 程序中利用最宽泛的数据类型,但他经常会有一些奇奇怪怪的问题。

如下代码:

func Foo(a [2]int) {a[0] = 8
}

func main() {a := [2]int{1, 2}
    Foo(a)       
    fmt.Println(a) 
}

输入结是什么。是 [8 2],对吗?

输入后果:

[1 2]

这是为什么,函数里批改了个寂寞?

解决办法

实际上在 Go 中,所有的函数传递都是值传递。也就是将数组传递给函数时,会复制该数组。
如果真的是须要传进函数内批改,能够改用切片。

如下代码:

func Foo(a []int) {if len(a) > 0 {a[0] = 8
    }
}

func main() {a := []int{1, 2}
    Foo(a)         
    fmt.Println(a)
}

输入后果:

[8 2]

起因是:切片不会存储任何的数据,他的底层 data 会指向一个底层数组。因而在批改切片的元素时,会批改其底层数组的相应元素,共享同一个底层数组的其余切片会一并批改。

你认为这就高枕无忧,解决了?并不。当切片扩容时,Go 底层会从新申请新的更大空间,存在与原有切片拆散的场景。

因而还是要及时将变更的值返回进去,在主流程上对立解决元数据会更好。

总结

在明天这篇文章中,咱们开始了 Go 常见编码谬误的第一节,共波及 5 个案例:

  1. nil Map。
  2. 空指针的援用。
  3. 应用对循环迭代器变量的援用。
  4. 在循环迭代器变量上应用 goroutine。
  5. 数组不会被扭转。

这些案例十分常见,在繁多代码上看会比拟容易察觉。但一旦混合到应用程序中,在繁冗代码里就比拟难看进去。

祝大家吸完后少踩坑,少出 BUG。

文章继续更新,能够微信搜【脑子进煎鱼了】浏览,本文 GitHub github.com/eddycjy/blog 已收录,学习 Go 语言能够看 Go 学习地图和路线,欢送 Star 催更。

举荐浏览

  • Go 语言入门系列:初探 Go 我的项目实战
  • Go 语言编程之旅:深刻用 Go 做我的项目
  • Go 语言设计哲学:理解 Go 的为什么和设计思考
  • Go 语言进阶之旅:进一步深刻 Go 源码

参考

  • 案例来自 golang/go/wiki/CommonMistakes
  • 案例来自 24 Common Mistakes in Go (gotchas) And How To Avoid Them

正文完
 0