- Published on
Go 语言编程陷阱解析:Select 死锁、数值溢出与并发安全问题
- Authors
- Name
- Liant
Go语言中的天坑
以下场景我们很有可能会遇到,需要深入理解才能避免出错
一、select可能存在的内存泄漏
select中case右侧如果是一个表达式,则会优先执行表达式的求值操作,多个case同时只会有一个被执行,需要注意这个特性。
- 会死锁的代码 以下代码会死锁:
package main
import "sync"
func main() {
var wg sync.WaitGroup
foo := make(chan int)
bar := make(chan int)
wg.Add(1)
go func() {
defer wg.Done()
select {
case foo <- <-bar:
default:
println("default")
}
}()
wg.Wait()
}
稍微改动一下就不会死锁,而会执行default分支:
package main
import "sync"
func main() {
var wg sync.WaitGroup
foo := make(chan int)
bar := make(chan int)
wg.Add(1)
go func() {
defer wg.Done()
select {
case x := <-bar:
foo <- x
default:
println("default")
}
}()
wg.Wait()
}
为什么会死锁?
当一个程序里面只有一个协程并且是阻塞状态,就会死锁。在上面的第一块代码中,case foo <- <-bar
右侧是一个表达式<-bar
,而不是一个值,会立即执行,bar
本身是一个管道,所以会直接阻塞,导致出现死锁。
- 内存泄漏 以下代码会造成内存泄漏:
package main
import "time"
func main() {
ch := make(chan int, 10)
go func() {
var i = 1
for {
i++
ch <- i
}
}()
for {
select {
case x := <- ch:
println(x)
case <- time.After(30 * time.Second):
println(time.Now().Unix())
}
}
}
为什么会内存泄漏?
select
中有两个case
,第二个case
右侧是一个表达式,会立即执行,返回一个chan
,而我们知道select
之后会选择其中的一个case执行,第一个<-ch
执行后,就会造成生成的time.After
管道永远不会被释放,造成内存泄漏,时间越长,泄漏越严重。
二、谨慎使用变量类型
- 直接上代码
以下代码会输出:4294967295
package main
import "fmt"
func main() {
var first uint32 = 1
var second uint32 = 2
fmt.Println(first - second)
}
为什么结果会出错?
计算机数值运算是以补码的形式计算完成的first
是一个正数,正、反、补码都是二进制本身,转换成补码也就是00000001
,second
是一个负数,补码是正数的二进制(去除第一位符号位)取反加一,转换成补码也就是1000 0010 -> 1111 1101 -> 1111 1110
对二进制进行求和:
0 0 0 0 0 0 0 1
+
1 1 1 1 1 1 1 0
---------------
1 1 1 1 1 1 1 1
将补码转换成原码:
为什么会是4294967295
?原因是我们代码中使用的是uint32
类型,两个无符号数加减的结果还是无符号数,也就是说11111111
最高位代表的不是符号,而是一个真实的数。又因为使用的uint32
占用4个字节,那么转换成原码也占用4个字节,变成这样1111 .... 1111
共32位,转换成10进制就成了4294967295
三、切片的复制
切片的复制需要考虑深拷贝和浅拷贝的问题。网上有很多文章,这里只是提示一下。
四、sync包的使用
当结构体内部嵌入了sync包的变量,需要注意使用指针传递,在网上找了一篇文章解释这个问题,传送门