Go语言中常见的十坑

Posted by jintang on 2019-02-01

以下10个问题都是非常常见的,很容易犯错。其中涉及到一些golang的知识点,在此也记录一下

1. defer的理解

下面内容输出什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14

package main
import (
"fmt"
)
func main() {
defer_call()
}
func defer_call() {
defer func() { fmt.Println("打印前") }()
defer func() { fmt.Println("打印中") }()
defer func() { fmt.Println("打印后") }()
panic("触发异常")
}

答案是:

1
2
3
4
答应后
答应中
答应前
触发异常

解释:

考察对defer的理解,defer函数属延迟执行,延迟到调用者函数执行 return 命令前被执行。多个defer之间按LIFO先进后出顺序执行

需要注意的是,函数的return value 不是原子操作.而是在编译器中分解为两部分:返回值赋值 和 return 。而defer刚好被插入到末尾的return前执行。故可以在derfer函数中修改返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import (
"fmt"
)
func doubleScore(source float32) (score float32) {
defer func() {
if score < 1 || score >= 100 {
//将影响返回值
score = source
}
}()
score = source * 2
return
//或者
//return source * 2
}
func main() {
fmt.Println(doubleScore(0)) //0
fmt.Println(doubleScore(20.0)) //40
fmt.Println(doubleScore(50.0)) //50
}

解释:

利用defer的”先进后执行”的特性,所以score = source*2会先执行,由于defer执行的函数使用score不是参数,而是外部变量,是会受外部修改影响的。

2. for遍历的理解

下面代码会输出什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main
import (
"fmt"
)
type student struct {
Name string
Age int
}
func pase_student() map[string]*student {
m := make(map[string]*student)
stus := []student{
{Name: "zhou", Age: 24},
{Name: "li", Age: 23},
{Name: "wang", Age: 22},
}
for _, stu := range stus {
m[stu.Name] = &stu
}
return m
}
func main() {
students := pase_student()
for k, v := range students {
fmt.Printf("key=%s,value=%v \n", k, v)
}
}

答案:

1
2
3
key=zhou,value=&{wang,22}
key=li,value=&{wang,22}
key=wang,value=&{wang,22}

解释:

由于for遍历时,stu是一个变量,每一次遍历都是这一个变量,只是每一趟遍历都拷贝了一遍struct的值给stu,而每一次遍历取&stu都是同一个变量的地址,所以m接收到的值是一样的地址。
当最后一次遍历,会把stu的值修改成{wang,22},所以m中所有的值都会是{wang,22}

如何修正?

1
2
3
4
for i, _ := range stus {
stu := stus[i]
m[stu.Name]=stu
}

3. 匿名函数的参数问题

下面代码会输出什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
func main() {
runtime.GOMAXPROCS(1)
wg := sync.WaitGroup{}
wg.Add(20)
for i := 0; i < 10; i++ {
go func() {
fmt.Println("i: ", i)
wg.Done()
}()
}
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Println("i: ", i)
wg.Done()
}(i)
}
wg.Wait()
}

答案:

1
第一个遍历都会输出10,第二个遍历会输出:1,2,3,4,5,6,7,8,9,10

解释:

由于第一个遍历里面的go func是没有参数的,i是外部变量,在主协程中的for i => 10很快就执行完了,所以go func输出都是遍历完之后的i=10
而第二个遍历中的i做为参数传入go func中,所以输出的值是顺序的

4. golang有组合但没有继承

下面代码输出什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import "fmt"

type People struct{}

func (p *People) ShowA() {
fmt.Println("showA")
p.ShowB()
}
func (p *People) ShowB() {
fmt.Println("showB")
}

type Teacher struct {
People
}

func (t *Teacher) ShowB() {
fmt.Println("teacher showB")
}
func main() {
t := Teacher{}
t.ShowA()
}

答案

1
2
showA
showB

解释:

Teacher组合了People,当调用t.ShowA()的时候,事实上是把People升级为Teacher的方法,但是实际上调用者是People,所以ShowA()调用的ShowB仍然属于People,故输出showB,而并非Teacher的showB()方法输出的teacher showB

另外,如果想要调用teacher的showB(),必须使用t.showB(),注意,调用者一定是Teacher。如果想要在外部调用People的showB,那么可以指定People这个组合成员

1
t.People.showB()

5. select的随机性

下面代码会触发异常吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
runtime.GOMAXPROCS(1)
int_chan := make(chan int, 1)
string_chan := make(chan string, 1)
int_chan <- 1
string_chan <- "hello"
select {
case value := <-int_chan:
fmt.Println(value)
case value := <-string_chan:
panic(value)
}
}

答案: 可能会,是随机事件

解释:

单个chan如果无缓冲时,将会阻塞。但结合 select可以在多个chan间等待执行。有三点原则:

  • select 中只要有一个case能return,则立刻执行。
  • 当如果同一时间有多个case均能return则伪随机方式抽取任意一个执行。
  • 如果没有一个case能return则可以执行”default”块。

此考题中的两个case中的两个chan均能return,则会随机执行某个case块。故在执行程序时,有可能执行第二个case,触发异常。

6. defer的先进后执行的特性(栈)

下面代码输出什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

func calc(index string, a, b int) int {
ret := a + b
fmt.Println(index, a, b, ret)
return ret
}
func main() {
a := 1 //line 1
b := 2 //2
defer calc("1", a, calc("10", a, b)) //3
a = 0 //4
defer calc("2", a, calc("20", a, b)) //5
b = 1 //6
}

答案:

1
2
3
4
10 1 2 3
20 0 2 2
2 0 2 2
1 1 2 3

解释:

在解题前需要明确两个概念:

  • defer是在函数末尾的return前执行,先进后执行,具体见问题1。
  • 函数调用时 int 参数发生值拷贝。
1
2
3
4
1. 首先,当执行第3行时,defer calc("1", 1, calc("10", 1, 2))应该先入栈,第三个参数是函数调用calc("10", a, b),先执行
2. 此时,a = 1, b = 2,传给calc("10",1, 2) => 3,到此第三行的函数为defer calc("1", 1, 3)就可以安心入栈了
3. 接下来第4行,a = 0,第5行与第三行同理,先计算calc("20", 0, 2) = 2,然后把calc("2", 0, 2)入栈
4. 最后,执行完第6行b=1后,main函数return,开始出栈了:先计算calc("2",0,2),最后计算calc("1",1,3)

7. 切片的初始化

下面代码输出什么?

1
2
3
4
5
func main() {
s := make([]int, 5)
s = append(s, 1, 2, 3)
fmt.Println(s)
}

答案:

1
[0,0,0,0,0,1,2,3]

解释:

由于s := make([]int, 5),会默认创建一个匿名的数组[0,0,0,0,0],当append时,由于数组只有5个元素,不够空间,则s = append(s,1,2,3)时,会重新创建一个数组,容量是原来的2倍,就是[0,0,0,0,0,0,0,0,0,0],然后把原来的匿名数组[0,0,0,0,0]拷贝过来,随后把1,2,3填入,最终:[0,0,0,0,0,1,2,3]

8. 注意map的并发访问的问题

下面代码有什么问题?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type UserAges struct {
ages map[string]int
sync.Mutex
}
func (ua *UserAges) Add(name string, age int) {
ua.Lock()
defer ua.Unlock()
ua.ages[name] = age
}
func (ua *UserAges) Get(name string) int {
if age, ok := ua.ages[name]; ok {
return age
}
return -1
}

答案:

1
在执行Get方法的时候可能会panic

解释:

虽然有使用sync.Mutex做写锁,但是map是并发读写不安全的。map属于引用类型,并发读写时多个协程见是通过指针访问同一个地址,即访问共享变量,此时同时读写资源存在竞争关系。会报错误信息:“fatal error: concurrent map read and map write”。

可以在在线运行中执行,复现该问题。那么如何改善呢? 当然Go1.9新版本中将提供并发安全的map。首先需要了解两种锁的不同:

sync.Mutex互斥锁
sync.RWMutex读写锁,基于互斥锁的实现,可以加多个读锁或者一个写锁。

利用读写锁可实现对map的安全访问,利用RWutex进行读锁。

参考改进版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type UserAges struct {
ages map[string]int
sync.RWMutex
}
func (ua *UserAges) Add(name string, age int) {
ua.Lock()
defer ua.Unlock()
ua.ages[name] = age
}
func (ua *UserAges) Get(name string) int {
ua.RLock()
defer ua.RUnlock()
if age, ok := ua.ages[name]; ok {
return age
}
return -1
}

9. 关于有缓冲的chan和无缓冲的chan的区别

下面代码会有什么问题?

1
2
3
4
5
6
7
8
9
10
11
12
func (set *threadSafeSet) Iter() <-chan interface{} {
ch := make(chan interface{})
go func() {
set.RLock()
for elem := range set.s {
ch <- elem
}
close(ch)
set.RUnlock()
}()
return ch
}

答案:内部迭代出现阻塞。默认初始化时无缓冲区,需要等待接收者读取后才能继续写入。

解释:

1
2
3
4
chan在使用make初始化时可附带一个可选参数来设置缓冲区。
默认无缓冲,题目中便初始化的是无缓冲区的chan,这样只有写入的元素直到被读取后才能继续写入,不然就一直阻塞。
设置缓冲区大小后,写入数据时可连续写入到缓冲区中,直到缓冲区被占满。
从chan中接收一次便可从缓冲区中释放一次。可以理解为chan是可以设置吞吐量的处理池。

值得注意的是,make(chan int, 1) 与 make(chan int) 是有区别的

1
2
无缓冲的 不仅仅是只能向 ch 通道放 一个值 而是一直要有人接收,那么ch <- elem才会继续下去,要不然就一直阻塞着,也就是说有接收者才去放,没有接收者就阻塞。
而缓冲为1则即使没有接收者也不会阻塞,因为缓冲大小是1只有当 放第二个值的时候 第一个还没被人拿走,这时候才会阻塞

再举个例子:

1
2
3
4
5
6
7
8
9
package main

import "fmt"

fun main() {
a := make(chan int) // 一个无缓冲区
a <- 1
fmt.Println("hello", <-a)
}

此代码会报异常:fatal error: all goroutines are asleep - deadlock!

这是由于在主协程中,检到有无缓冲channel在被写入,但此前不存有人接受(即使后面有fmt.Println(“hello”, <-a)),所以出现阻塞,go运行时判断为deadlock

如果代码改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

fun main() {
a := make(chan int)
go func() {
a <- 1
}()
go func() {
fmt.Println(<-a)
}()
fmt.Println("hello")
}

此时有另一个协程负责接受,代码就正常了

10. 实现接口的问题

下面代码能编译通过吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
import (
"fmt"
)
type People interface {
Speak(string) string
}
type Stduent struct{}
func (stu *Stduent) Speak(think string) (talk string) {
if think == "bitch" {
talk = "You are a good boy"
} else {
talk = "hi"
}
return
}
func main() {
var peo People = Stduent{}
think := "bitch"
fmt.Println(peo.Speak(think))
}

答案:不能, 值类型 Student{} 未实现接口People的方法,不能定义为 People类型

解释:

为什么没有实现接口People呢?

func (stu Stduent) Speak(think string) (talk string) 是表示结构类型Student的指针有提供该方法,但该方法并不属于结构类型Student的方法。因为struct是值类型。

修改方法:

定义为指针 go var peo People = &Stduent{}
方法定义在值类型上,指针类型本身是包含值类型的方法。 go func (stu Stduent) Speak(think string) (talk string) { //... }