Go 语言的那些坑

阅读数:145 2019 年 9 月 23 日 09:37

Go语言的那些坑

1. 背景

Golang 是我最喜欢的一门语言,它简洁、高效、易学习、开发效率高、还可以编译成机器码。虽然它一出世,就饱受关注,而且现在在市面上逐渐流行开来,但是,它毕竟是一门新兴语言,还有很多让人不太习惯的地方(即坑),我们一边学习,一边踩坑,希望对大家有借鉴作用。

2. 详述

1)文件名字不要轻易以 __test.go 为结尾

Golang 的 source 文件的命名和其他语言本无差别,但是 Golang 自带 Unit test,它的 unit test 有个小规范:所有 unit test 文件都要以 __test.go 为结尾!
所以,当你命名一个非 unit test 文件为 XXX_test.go,而且执意要编译时,就会报错:no buildable Go source files in XXXXXX(你的文件路径)。
所以,切记,以 __test.go 为结尾的都是 unit test 的文件,且切记不要把 unit test 文件和普通 Go 文件放到一起,一定要把 unit test 文件集体放到一个目录中,否则会编译不过的。

2)语句 fmt.Println(“这里是汉字:” + 字符串变量) 字符串变量的值打印不出来的问题
现有如下程序:

复制代码
1package main
2
3import "fmt"
4
5func main() {
6 m1 := getString()
7
8 fmt.Println(" 现在是:" + m1)
9}
10
11func getString()string{
12 return "abd"
13}

运行指令 go run test.go

Go语言的那些坑

是单独打印变量 m1 却可以正常显示

复制代码
1import "fmt"
2
3func main() {
4 m1 := getString()
5
6 fmt.Println(m1)
7
8 fmt.Println(" 现在是:" + m1)
9}
10
11func getString()string{
12 return "abd"
13}

Go语言的那些坑

是为什么呢?很奇怪啊!

其实这要怪 IDE,我的 IDE 是 phpstorm + Golang 插件包,IDE 自带的 console 对中文的支持很不友好,带中文的字符串打印出来后,容易显示不全,其实通过 terminal 打印出来,是正确的!

Go语言的那些坑

3)多个 defer 出现的时候,多个 defer 之间按照 LIFO(后进先出)的顺序执行

复制代码
1package main
2
3import "fmt"
4
5func main(){
6 defer func(){
7 fmt.Println("1")
8 }()
9
10 defer func(){
11 fmt.Println("2")
12 }()
13
14 defer func(){
15 fmt.Println("3")
16 }()
17
18
19}

对应的输出是:

复制代码
13
22
31

4)panic 中可以传任何值,不仅仅可以传 string

复制代码
1package main
2
3import "fmt"
4
5func main(){
6
7 defer func(){
8 if r := recover();r != nil{
9 fmt.Println(r)
10 }
11 }()
12
13 panic([]int{12312})
14}

输出:

复制代码
1[12312]

5)用 for range 来遍历数组或者 map 的时候,被遍历的指针是不变的,每次遍历仅执行 struct 值的拷贝

复制代码
1import "fmt"
2
3type student struct{
4 Name string
5 Age int
6}
7
8func main(){
9 var stus []student
10
11 stus = []student{
12 {Name:"one", Age: 18},
13 {Name:"two", Age: 19},
14 }
15
16 data := make(map[int]*student)
17
18 for i, v := range stus{
19 data[i] = &v // 应该改为:data[i] = &stus[i]
20 }
21
22 for i, v := range data{
23 fmt.Printf("key=%d, value=%v \n", i,v)
24 }
25}

所以,结果输出为:

复制代码
1key=0, value=&{two 19}
2key=1, value=&{two 19}

6)Go 中没有继承!没有继承!Go 中是叫组合!是组合!

复制代码
1import "fmt"
2
3type student struct{
4 Name string
5 Age int
6}
7
8func (p *student) love(){
9 fmt.Println("love")
10
11}
12
13func (p *student) like(){
14 fmt.Println("like first")
15 p.love()
16}
17
18type boy struct {
19 student
20}
21
22func (b * boy) love(){
23 fmt.Println("hate")
24}
25
26func main(){
27
28 b := boy{}
29
30 b.like()
31}

输出:

复制代码
1like first
2love

7)不管运行顺序如何,当参数为函数的时候,要先计算参数的值

复制代码
1func main(){
2 a := 1
3 defer print(function(a))
4 a = 2;
5}
6
7func function(num int) int{
8 return num
9}
10func print(num int){
11 fmt.Println(num)
12}

输出:

复制代码
11

8)注意是 struct 的函数,还是 * struct 的函数

复制代码
1import "fmt"
2
3type people interface {
4 speak()
5}
6
7type student struct{
8 name string
9 age int
10}
11func (stu *student) speak(){
12 fmt.Println("I am a student, I am ", stu.age)
13}
14
15
16func main(){
17 var p people
18 p = student{name:"RyuGou", age:12} // 应该改为 p = &student{name:"RyuGou", age:12}
19 p.speak()
20}

输出:

复制代码
1cannot use student literal (type student) as type people in assignment:
2student does not implement people (speak method has pointer receiver)

9)make(chan int) 和 make(chan int, 1) 是不一样的
chan 一旦被写入数据后,当前 goruntine 就会被阻塞,知道有人接收才可以(即 " <- ch"),如果没人接收,它就会一直阻塞着。而如果 chan 带一个缓冲,就会把数据放到缓冲区中,直到缓冲区满了,才会阻塞

复制代码
1import "fmt"
2
3
4func main(){
5 ch := make(chan int) // 改为 ch := make(chan int, 1) 就好了
6
7 ch <- 1
8
9 fmt.Println("success")
10}

输出:

复制代码
1fatal error: all goroutines are asleep - deadlock!

10)golang 的 select 的功能和 select, poll, epoll 相似, 就是监听 IO 操作,当 IO 操作发生时,触发相应的动作。
select 的代码形式和 switch 非常相似, 不过 select 的 case 里的操作语句只能是 "IO 操作 "(不仅仅是取值 <-channel,赋值 channel<- 也可以), select 会一直等待等到某个 case 语句完成,也就是等到成功从 channel 中读到数据。 则 select 语句结束

复制代码
1 import "fmt"
2
3
4func main(){
5 ch := make(chan int, 1)
6
7 ch <- 1
8
9 select {
10 case msg :=<-ch:
11 fmt.Println(msg)
12 default:
13 fmt.Println("default")
14 }
15
16 fmt.Println("success")
17}
输出:
11
2success
default 可以判断 chan 是否已经满了
1import "fmt"
2
3
4func main(){
5 ch := make(chan int, 1)
6
7 select {
8 case msg :=<-ch:
9 fmt.Println(msg)
10 default:
11 fmt.Println("default")
12 }
13
14 fmt.Println("success")
15}

输出:

复制代码
1default
2success

此时因为 ch 中没有写入数据,为空,所以 case 不会读取成功。 则 select 执行 default 语句。

11)Go 语言中不存在未初始化的变量
变量定义基本方式为:

1var 发量名字 类型 = 表达式

其中类型和表达式均可省略,如果初始化表达式被省略,将用零值初始化该变量。

数值变量对应的是 0 值

布尔变量对应的是 false

字符串对应的零值是空字符串

接口或者引用类型(包括 slice,map,chan)变量对应的是 nil

数组或者结构体等聚合类型对应的零值是每个元素或字段对应该类型的零值。

复制代码
1 var s string
2 fmt.Println(s) // ""

12):= 注意的问题
使用:= 定义的变量,仅能使用在函数内部。

在定义多个变量的时候:= 周围不一定是全部都是刚刚声明的,有些可能只是赋值,例如下面的 err 变量

复制代码
Go<br />in, err := os.Open(infile)<br />// TODO<br />out, err := os.Create(outfile)<br />

13)new 在 Go 语言中只是一个预定义的函数,它并不是一个关键字,我们可以将 new 作为变量或者其他
例如:

复制代码
1func delta(old, new int) int {
2 return new - old
3}

以上是正确的。

14)并不是使用 new 就一定会在堆上分配内存
编译器会自动选择在栈上还是在堆上分配存储空间,但可能令人惊讶的是,这个选择并不是由用 var 还是 new 声明变量的方式决定的。

请看例子:

复制代码
1var global *int
2
3func f() {
4 var x int x=1
5 global = &x
6}
7
8func g() {
9 y := new(int)
10 *y = 1
11}

f() 函数中的 x 就是在堆上分配内存,而 g() 函数中的 y 就是分配在栈上。

15)init 函数在同一个文件中可以包含多个
在同一个包文件中,可以包含有多个 init 函数,多个 init 函数的执行顺序和定义顺序一致。

16)Golang 中没有“对象”

复制代码
1package main
2
3import (
4 "fmt"
5)
6type test struct {
7 name string
8}
9func (t *test) getName(){
10 fmt.Println("hello world")
11}
12func main() {
13 var t *test
14 t = nil
15 t.getName()
16}

能正常输出吗?会报错吗?

输出为:

复制代码
1hello world

可以正常输出。Go 本质上不是面向对象的语言,Go 中是不存在 object 的含义的,Go 语言书籍中的对象也和 Java、PHP 中的对象有区别,不是真正的”对象”,是 Go 中 struct 的实体。

调用 getName 方法,在 Go 中还可以转换,转换为:Type.method(t Type, arguments)
所以,以上代码 main 函数中还可以写成:

复制代码
1func main() {
2 (*test).getName(nil)
3}

17)Go 中的指针 * 符号的含义
& 的意思大家都明白的,取地址,假如你想获得一个变量的地址,只需在变量前加上 & 即可。

例如:

复制代码
1a := 1
2b := &a

现在,我拿到 a 的地址了,但是我想取得 a 指针指向的值,该如何操作呢?用 * 号,*b 即可。
* 的意思是对指针取值。

下面对 a 的值加一

复制代码
1a := 1
2b := &a
3*b++

和 & 可以相互抵消,同时注意,& 可以抵消,但是 &不可以;所以 a 和&a 是一样的,和 *&&&a 也是一样的。

18)os.Args 获取命令行指令参数,应该从数组的 1 坐标开始
os.Args 的第一个元素,os.Args[0], 是命令本身的名字

复制代码
1package main
2import (
3 "fmt"
4 "os"
5)
6func main() {
7 fmt.Println(os.Args[0])
8}

以上代码,经过 go build 之后,打包成一个可执行文件 main,然后运行指令./main 123

输出:./main

19)数组切片 slice 的容量问题带来的 bug
请看下列代码:

复制代码
1import (
2 "fmt"
3)
4func main(){
5 array := [4]int{10, 20, 30, 40}
6 slice := array[0:2]
7 newSlice := append(slice, 50)
8 newSlice[1] += 1
9 fmt.Println(slice)
10}

请问输出什么?
答案是:

复制代码
1[10 21]

如果稍作修改,将以上 newSlice 改为扩容三次,newSlice := append(append(append(slice, 50), 100), 150) 如下:

复制代码
1import (
2 "fmt"
3)
4func main(){
5 array := [4]int{10, 20, 30, 40}
6 slice := array[0:2]
7 newSlice := append(append(append(slice, 50), 100), 150)
8 newSlice[1] += 1
9 fmt.Println(slice)
10}

输出为:

复制代码
1[10 20]

这特么是什么鬼?
这就要从 Golang 切片的扩容说起了;切片的扩容,就是当切片添加元素时,切片容量不够了,就会扩容,扩容的大小遵循下面的原则:(如果切片的容量小于 1024 个元素,那么扩容的时候 slice 的 cap 就翻番,乘以 2;一旦元素个数超过 1024 个元素,增长因子就变成 1.25,即每次增加原来容量的四分之一。)如果扩容之后,还没有触及原数组的容量,那么,切片中的指针指向的位置,就还是原数组(这就是产生 bug 的原因);如果扩容之后,超过了原数组的容量,那么,Go 就会开辟一块新的内存,把原来的值拷贝过来,这种情况丝毫不会影响到原数组。
建议尽量避免 bug 的产生。

20)map 引用不存在的 key,不报错
请问下面的例子输出什么,会报错吗?

复制代码
1import (
2 "fmt"
3)
4
5func main(){
6 newMap := make(map[string]int)
7 fmt.Println(newMap["a"])
8}

答案是:

复制代码
10

不报错。不同于 PHP,Golang 的 map 和 Java 的 HashMap 类似,Java 引用不存在的会返回 null,而 Golang 会返回初始值

21)map 使用 range 遍历顺序问题,并不是录入的顺序,而是随机顺序
请看下面的例子:

复制代码
1import (
2 "fmt"
3)
4
5func main(){
6 newMap := make(map[int]int)
7 for i := 0; i < 10; i++{
8 newMap[i] = i
9 }
10 for key, value := range newMap{
11 fmt.Printf("key is %d, value is %d\n", key, value)
12 }
13}

输出:

复制代码
1key is 1, value is 1
2key is 3, value is 3
3key is 5, value is 5
4key is 7, value is 7
5key is 9, value is 9
6key is 0, value is 0
7key is 2, value is 2
8key is 4, value is 4
9key is 6, value is 6
10key is 8, value is 8

是杂乱无章的顺序。map 的遍历顺序不固定,这种设计是有意为之的,能为能防止程序依赖特定遍历顺序。

22)channel 作为函数参数传递,可以声明为只取 (<- chan) 或者只发送 (chan <-)
一个函数在将 channel 作为一个类型的参数来声明的时候,可以将 channl 声明为只可以取值 (<- chan) 或者只可以发送值 (chan <-),不特殊说明,则既可以取值,也可以发送值。

例如:只可以发送值

复制代码
1func setData(ch chan <- string){
2 //TODO
3}

如果在以上函数中存在 <-ch 则会编译不通过。

如下是只可以取值:

复制代码
1func setData(ch <- chan string){
2 //TODO
3}

如果以上函数中存在 ch<- 则在编译期会报错

23)使用 channel 时,注意 goroutine 之间的执行流程问题

复制代码
1package main
2import (
3 "fmt"
4)
5func main(){
6 ch := make(chan string)
7 go setData(ch)
8 fmt.Println(<-ch)
9 fmt.Println(<-ch)
10 fmt.Println(<-ch)
11 fmt.Println(<-ch)
12 fmt.Println(<-ch)
13}
14func setData(ch chan string){
15 ch <- "test"
16 ch <- "hello wolrd"
17 ch <- "123"
18 ch <- "456"
19 ch <- "789"
20}

以上代码的执行流程是怎样的呢?
一个基于无缓存 channel 的发送或者取值操作,会导致当前 goroutine 阻塞,一直等待到另外的一个 goroutine 做相反的取值或者发送操作以后,才会正常跑。
以上例子中的流程是这样的:

主 goroutine 等待接收,另外的那一个 goroutine 发送了“test”并等待处理;完成通信后,打印出”test”;两个 goroutine 各自继续跑自己的。
主 goroutine 等待接收,另外的那一个 goroutine 发送了“hello world”并等待处理;完成通信后,打印出”hello world”;两个 goroutine 各自继续跑自己的。
主 goroutine 等待接收,另外的那一个 goroutine 发送了“123”并等待处理;完成通信后,打印出”123”;两个 goroutine 各自继续跑自己的。
主 goroutine 等待接收,另外的那一个 goroutine 发送了“456”并等待处理;完成通信后,打印出”456”;两个 goroutine 各自继续跑自己的。
主 goroutine 等待接收,另外的那一个 goroutine 发送了“789”并等待处理;完成通信后,打印出”789”;两个 goroutine 各自继续跑自己的。

记住:Golang 的 channel 是用来 goroutine 之间通信的,且通信过程中会阻塞。

作者介绍:
刘刚,贝壳找房研发工程师,目前负责贝壳找房运维开发工作。

本文转载自公众号贝壳产品技术(ID:gh_9afeb423f390)。

原文链接:

https://mp.weixin.qq.com/s/14WJ6FMNX9YQm26aaHDE_w

评论

发布