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
未完待续……