golang常见陷阱

基本

内置的数据结构操作是无锁的
虽然 Go 语言天生高并发,但内置的数据结构都是非线程安全的。为了实现“原子化”的数据操作,开发者需要自己对数据操作进行加锁。channel 和 sync 都是手动加锁的好方案。

自加减
go中没有前置的自加减运算符。并且后置的自加减不能与其他表达式出现在同一语句中。

1
2
3
4
i := 0
++i // syntax error: unexpected ++
fmt.Println(data[i++]) // syntax error: unexpected ++, expecting :
i++ // ok

数据比较
可以用==做数据比较的有:

  • 基本数据类型:bool, byte, ruue, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, complex, complex64, complex128, string, 指针类型
  • 基本数据类型的数组。
  • 只包含基本数据类型的数据结构。

对于不能用==比较的,可以使用reflect.DeepEqual()方法来实现。

对于字符串([]byte或者string)的比较,若不区分大小写,可以使用bytesstrings包的ToUpper()或者ToLower()函数预处理。对于英语文本,这么做是没问题的,但对于许多其他的语言来说就不行了。这时应该使用 strings.EqualFold()bytes.EqualFold()

如果你的byte slice中包含需要验证用户数据的隐私信息(比如,加密哈希、tokens等),不要使用 reflect.DeepEqual()bytes.Equal(),或者 bytes.Compare(),因为这些函数将会让你的应用易于被定时攻击。为了避免泄露时间信息,使用 'crypto/subtle'包中的函数(即, subtle.ConstantTimeCompare())。

字符串

String类型的不可为nil
string类型的零值是””,而不是nil。将string类型变量赋值为nil,或者与nil做比较,会得到编译错误

1
2
3
4
var s string
s = nil // cannot use nil as type string in assignment
if s == nil { // invalid operation: x == nil (mismatched types string and nil)
}

String不可变

String 是带有一些附加属性的只读的字节片,不能对其中的单个字符进行修改

1
2
x := "text"
x[0] = 'T' // cannot assign to x[0]

String的长度

1
2
3
s := "你好"
fmt.Println(len(s)) // 6
fmt.Println(utf8.RuneCountInString(s)) // 2

集合

可以对nil的slice进行添加操作,但是map不行

1
2
3
4
5
var s []int
var m map[int]string

s = append(s, 1) // ok
m[1] = "one" // panic

数组、切片的for…range返回是两个值
每次迭代返回两个值,第一个是索引号,第二个才是元素数据。

1
2
3
4
x := []string{"a","b","c"}
for v := range x {
fmt.Println(v) //prints 0, 1, 2
}

数组、切片的for…range的返回值是元素数据的副本
对range返回的值进行修改,并不会影响集合中的原数据。

1
2
3
4
5
6
s := []int{1, 2, 3}
for _, v := range s {
v = 10
_ = v
}
fmt.Println(s) // prints [1 2 3]

good code:

1
2
3
4
5
s := []int{1, 2, 3}
for i := range s {
s[i] = 10
}
fmt.Println(s) // prints [10 10 10]

slice中的隐藏数据
当你重新划分一个slice时,新的slice将引用原有slice的数组。如果你忘了这个行为的话,在你的应用分配大量临时的slice用于创建新的slice来引用原有数据的一小部分时,会导致难以预期的内存使用。
bad code:

1
2
3
4
func get() []byte {  
raw := make([]byte,10000)
return raw[:3]
}

为了避免这个陷阱,你需要从临时的slice中拷贝数据(而不是重新划分slice)

1
2
3
4
5
6
func get() []byte {  
raw := make([]byte,10000)
res := make([]byte,3)
copy(res,raw[:3])
return res
}

slice数据被破坏
slice重新划分后,引用的是原来的数组,对新slice的修改,会影响到原数据。

1
2
3
4
5
6
7
8
base := []int{1, 2, 3, 4}
s1 := base[:2]
s2 := base[:3]

s2[0] = 11

fmt.Println(s1) // print: [11 2]
fmt.Println(s2) // print: [11 2 3]

复合字面量的多行表示时缺少逗号
复合字面量(数组、切片、字典、结构体)最后元素不跟上}需要加上,

1
2
3
4
5
_ = []int{1, 2}
_ = []int{
1,
2 // syntax error: need trailing comma before newline in composite literal
}

更新map的值
如果你有一个struct值的map,你无法更新单个的struct值。

1
2
3
4
5
6
type data struct {  
name string
}

m := map[string]data {"x":{"one"}}
m["x"].name = "two" //error: cannot assign to m["x"].name

good code:

1
2
3
v := m["x"]
v.name = "two"
m["x"] = v

控制

跳出多层循环

1
2
3
4
5
6
loop:
for i:=0; i<3; i++ {
for j:=0; j<3; j++ {
break loop
}
}

for中的迭代变量和闭包
for语句中的迭代变量在每次迭代时被重新使用。这就意味着你在 for循环中创建的闭包(即函数字面量)将会引用同一个变量(而在那些goroutine开始执行时就会得到那个变量的值)

1
2
3
4
5
6
7
8
data := []int{1, 2, 3}
for _,v := range data {
go func() {
fmt.Println(v)
}()
}
time.Sleep(3 * time.Second)
//goroutines print: 3, 3, 3

修正:

1
2
3
4
5
6
7
8
data := []int{1, 2, 3}
for _,v := range data {
go func(x int) {
fmt.Println(x)
}(v)
}
time.Sleep(3 * time.Second)
//goroutines print: 1, 2, 3

switch语句中的Fallthrough
不同于其他语言,go中的switch中的每个case语句执行完后默认终止,而不需要break来终止。若希望继续执行下一个case,加上fallthrough

1
2
3
4
5
6
7
8
9
10
11
isSpace := func(ch byte) bool {
switch(ch) {
case ' ': // error hear
case '\t':
return true
}
return false
}

fmt.Println(isSpace('\t')) //prints true (ok)
fmt.Println(isSpace(' ')) //prints false (not ok)

修正:

1
2
3
4
5
6
switch(ch) {
case ' ':
fallthrough
case '\t':
return true
}

go中的一个case中允许多个表达式,所以还可以这么写:

1
2
3
4
switch(ch) {
case ' ', '\t':
return true
}

函数

函数参数的数组是值传递
函数调用参数的数组是对原数组的拷贝,函数中修改数组,不影响原数组。

1
2
3
4
5
6
x := [3]int{1,2,3}
func(arr [3]int) {
arr[0] = 7
fmt.Println(arr) //prints [7 2 3]
}(x)
fmt.Println(x) //prints [1 2 3] (not ok if you need [7 2 3])

可以使用数组指针或者切片实现函数中修改原数组

defer函数调用参数的求值
被defer的函数的参数会在defer声明时求值,而不是在函数实际执行时。

1
2
3
var i int = 1
defer fmt.Println(i) // print: 1
i++

通道

关闭的channel的读写
对关闭的channel,读操作得到零值;写操作触发异常

nil channel的读写
读写nil channel永远阻塞

1
2
3
4
5
var ch chan int
go func() {
<- ch
}()
ch <- 1
1
2
3
4
5
var ch chan int
go func() {
ch <- 1
}()
<- ch

输出:

1
2
3
4
5
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive (nil chan)]:
...
goroutine 5 [chan send (nil chan)]:

并发

应用退出时协程未结束
应用程序退出时并不会等待线程完成结束。

错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
workFun := func(id int) {
defer wg.Done()
fmt.Printf("work [%v] start\n", id)
time.Sleep(time.Second)
fmt.Printf("work [%v] over\n", id)
}

for i := 0; i < 2; i++ {
go workFun(i)
}

fmt.Println("Bye")

输出:
Bye

修正:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var wg sync.WaitGroup

workFun := func(id int) {
defer wg.Done()
fmt.Printf("work [%v] start\n", id)
time.Sleep(time.Second)
fmt.Printf("work [%v] over\n", id)
}

for i := 0; i < 2; i++ {
wg.Add(1)
go workFun(i)
}

wg.Wait()
fmt.Println("Bye")

输出:
work [0] start
work [1] start
work [0] over
work [1] over
Bye

阻塞的Goroutine和资源泄露

1
2
3
4
5
6
7
8
func First(query string, replicas ...Search) Result {  
c := make(chan Result)
searchReplica := func(i int) { c <- replicas[i](query) }
for i := range replicas {
go searchReplica(i)
}
return <-c
}

函数启动多工作流并发进行搜索功能,第一个搜索成功的协程将结果发送到结果通道c处。那其他协程呢?将会阻塞而导致资源的泄露。
不错的解决方法是使用一个有 default情况的 select语句和一个保存一个缓存结果的channel。 default情况保证了即使当结果channel无法收到消息的情况下,goroutine也不会堵塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
func First(query string, replicas ...Search) Result {  
c := make(chan Result,1)
searchReplica := func(i int) {
select {
case c <- replicas[i](query):
default:
}
}
for i := range replicas {
go searchReplica(i)
}
return <-c
}

协程调度
有可能会出现这种情况,一个无耻的goroutine阻止其他goroutine运行。当你有一个不让调度器运行的 for循环时,这就会发生。

1
2
3
4
5
6
7
8
done := false

go func(){
done = true
}()

for !done { }
fmt.Println("done!")

只要for循环中没有会触发调度的代码,主协程就永远占用CPU而不让其他协程被调度。该例子在单核下会死循环,若在多核机子上运行,可以添加runtime.GOMAXPROCS(1)来限制工作数。

HTTP

关闭HTTP响应
使用标准http库发起请求时,得到一个httpx响应变量。处理完后需要关闭响应body,即使没有读取它。还有需要注意的是关闭的地方。如:

1
2
3
4
5
6
7
resp, err := http.Get("https://api.ipify.org?format=json")
// defer resp.Body.Close() // 1: not here
if err != nil {
return err
}
defer resp.Body.Close() // 2: ok here
/* 响应处理... */

大多情况下,HTTP响应失败时,resp变量是nil,err是non-nil。在1处关闭会引发runtime panic。

参考资料

http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/

显示 Gitment 评论