Published on

Go 语言编程陷阱解析:Select 死锁、数值溢出与并发安全问题

Authors
  • avatar
    Name
    Liant
    Twitter

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是一个正数,正、反、补码都是二进制本身,转换成补码也就是00000001second是一个负数,补码是正数的二进制(去除第一位符号位)取反加一,转换成补码也就是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包的变量,需要注意使用指针传递,在网上找了一篇文章解释这个问题,传送门