hello world

stay foolish, stay hungry

golang 踩坑总结

groutine 泄漏

goroutine 是用户态的轻量级协程,在 golang 中开启 groutine 开销很小,能轻松开启成千上万的 goroutine。但是如果开启的 groutine 长驻内存,并且还不断的开启新的 goroutine,那么可能会导致内存泄漏问题。

程序 1 展示了一种常见的 goroutine 泄漏的 case。leak 函数中,创建一个 channel,并启动一个 goroutine 来消费这个 channel。这个 goroutine 结束的唯一条件是关闭 ch。但是 leak 函数返回之后,ch 没有被关闭,这样 goroutine 会一直在内存中,随着 leak 函数调用次数的增多,占用内存也随着增长,最终导致内存泄漏。

// 程序 1 
func leak() {
    ch := make(chan int)
    go func() {
        for range ch { }
    }()    
}

开启 goroutine 之前,必须要清楚 goroutine 的退出条件。

在循环中 goroutine func 直接使用字面量

这是刚开始写 goroutine 时特别容易犯的一个问题,在循环中 goroutine func 直接使用字面量,可能会导致一些不可预期的行为。简单看一个 demo。程序 2 中,原想输出 array 中的值,但最终输出的可能不确定,在我的机器上是 55555。这段代码在 IDE 中会有 Loop variables captured by 'func' literals in 'go' statements might have unexpected values 警告,产生这问题的原因是 goroutine 中的匿名函数执行的时机时不确定的,执行匿名函数时,a 的值可能已经改变了。

// 程序 2 
func main() {
    array := []int{1, 2, 3, 4, 5}

    for _, a := range array {
        go func() {
            fmt.Print(a)
        }()
    }

    time.Sleep(time.Second)
}

修改方式有很多,可以将 a 赋值另一个变量,如 程序 3 所示,用 i 暂存 a 的值,这样就可以输出预期的结果。

// 程序 3 
func main() {
    array := []int{1, 2, 3, 4, 5}

    for _, a := range array {
        i := a
        go func() {
            fmt.Println(i)
        }()
    }

    time.Sleep(time.Second)
}

或者将 a 作为参数传递给 goroutine 的匿名函数,如程序 4 所示。

// 程序 4 
func main() {
    array := []int{1, 2, 3, 4, 5}

    for _, a := range array {
        go func(i int) {
            fmt.Println(i)
        }(a)
    }

    time.Sleep(time.Second)
}

close a closed channel

golang 中 channel 不能重复 close,这应该是大家都知道的问题。这里就不再赘述。golang 中 channel 可以不关闭,如果 channel 不再被使用,即使不关闭也会被回收。通常 close channel 会作为 channel 不会再有数据的控制信号,如果接收方不关心 channel 中是否还会有数据,那么没必要主动关闭 channel。如果要关闭 channel,则最好是由发送方来关闭。

Note that it is only necessary to close a channel if the receiver is looking for a close. Closing the channel is a control signal on the channel indicating that no more data follows.

关于 channel 还有以下几个点需要注意:

  • channel 的零值是 nil,使用前需要先初始化
  • 向未初始化的 channel 发送数据会 block
  • 从未初始化的 channel 读数据会 block
  • 向已经 closed 的 channel 发数据会 panic
  • 从已经 closed 的 channel 读数据不会 panic,会读到 channel 中未被读取的数据或默认值

make 的三种数据类型

make 函数可以用于创建 channel、slice、map。其中 channel 必须先初始化;而 map 则可以只做声明;而如果要用 index 操作 slice,则 slice 也必须初始化,而使用 append 则可以不用初始化。

处于对性能的考虑,使用 map 或 slice 时,如果能提前预估容量,则推荐先初始化再使用,并且在初始化的时候指定容量。

深 copy or 浅 copy

深 copy 会重新开辟内存空间并创建一个一样的对象,新对象和原来的对象不会共享内存空间。浅 copy 只会 copy 数据的地址,新对象和原来的对象指向同一块内存空间。

先说结论,值类型默认深 copy,比如 int、 string、数组、结构体等;引用类型默认浅 copy,比如 slice、map等。

再来看几个示例。

程序 5 展示了数组类型深拷贝的示例,虽然将 a 赋值给 b,但 a 和 b 是不同的数组,修改 b 并不会影响 a。

// 程序 5
func main() {
    a := [3]int{1, 2, 3}
    b := a
    fmt.Printf("a: %+v, %p \n", a, &a) // a: [1 2 3], 0xc00001c0a8 
    fmt.Printf("b: %+v, %p \n", b, &b) // b: [1 2 3], 0xc00001c0c0 
    b[0] = 0
    fmt.Printf("a: %+v, %p \n", a, &a) // a: [1 2 3], 0xc00001c0a8 
    fmt.Printf("b: %+v, %p \n", b, &b) // b: [0 2 3], 0xc00001c0c0
}

程序 6 展示了 slice 类型浅拷贝的示例,a 和 b 指向同一个地址,如果修改了 b ,那么 a 也会随着变。

// 程序 6
func main() {
    a := []int{1, 2, 3}
    b := a
    fmt.Printf("a: %+v, %p \n", a, a) // a: [1 2 3], 0xc0000b4018
    fmt.Printf("b: %+v, %p \n", b, b) // b: [1 2 3], 0xc0000b4018
    b[0] = 0
    fmt.Printf("a: %+v, %p \n", a, a) // a: [0 2 3], 0xc0000b4018
    fmt.Printf("b: %+v, %p \n", b, b) // b: [0 2 3], 0xc0000b4018
}

slice 作为 参数

slice 中的 array 是指针类型,作为参数传递的是指针的值,但是 slice 是一个结构体,并不完全是一个指针,slice 的结构如程序 7 所示。

// 程序 7
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

看下面这个示例,想在 appendSlice 函数中将元素 append 到 a,但最终输出的结果为 [1, 2, 3],而并不是我们期望的结果。再回过来看 slice 的结构,其中的 array 是指针,而 len 和 cap 则是普通的 int 值,所以 slice 作为函数参数时,修改其长度,可能会得到非预期的结果。

// 程序 8
func main() {
    a := []int{1, 2, 3}
    appendSlice(a, 4)
    fmt.Println(a) // [1, 2, 3]
}

func appendSlice(s []int, a int) {
    s = append(s, a)
}

int/uint 的长度不是固定的

int 数据类型的长度不是固定的,长度和平台有关,可能是 32 位,也可能是 64 位。

中文字符串长度问题

计算中文字符串长度的时,不能简单的用 len() 函数,而应该使用 utf8.RuneCountInString(),或者先转换为 rune 数组,然后计算数组长度。golang 中字符串是字节数组,len() 函数用于计算字符串字节长度,utf8.RuneCountInString() 会计算字符长度。

s1 := "Hello, World!"
fmt.Println(len(s1)) //13
s2 := "你好,世界!"
fmt.Println(len(s2)) // 18
fmt.Println(utf8.RuneCountInString(s2)) // 6
rs := []rune(s2)
fmt.Println(len(rs)) // 6

未完待续……