基本
内置的数据结构操作是无锁的
虽然 Go 语言天生高并发,但内置的数据结构都是非线程安全的。为了实现“原子化”的数据操作,开发者需要自己对数据操作进行加锁。channel 和 sync 都是手动加锁的好方案。
自加减
go中没有前置的自加减运算符。并且后置的自加减不能与其他表达式出现在同一语句中。
1 | i := 0 |
数据比较
可以用==
做数据比较的有:
- 基本数据类型:bool, byte, ruue, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, complex, complex64, complex128, string, 指针类型
- 基本数据类型的数组。
- 只包含基本数据类型的数据结构。
对于不能用==
比较的,可以使用reflect.DeepEqual()
方法来实现。
对于字符串([]byte或者string)的比较,若不区分大小写,可以使用bytes
和strings
包的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 | var s string |
String不可变
String 是带有一些附加属性的只读的字节片,不能对其中的单个字符进行修改
1 | x := "text" |
String的长度
1 | s := "你好" |
集合
可以对nil的slice进行添加操作,但是map不行
1 | var s []int |
数组、切片的for…range返回是两个值
每次迭代返回两个值,第一个是索引号,第二个才是元素数据。
1 | x := []string{"a","b","c"} |
数组、切片的for…range的返回值是元素数据的副本
对range返回的值进行修改,并不会影响集合中的原数据。
1 | s := []int{1, 2, 3} |
good code:
1 | s := []int{1, 2, 3} |
slice中的隐藏数据
当你重新划分一个slice时,新的slice将引用原有slice的数组。如果你忘了这个行为的话,在你的应用分配大量临时的slice用于创建新的slice来引用原有数据的一小部分时,会导致难以预期的内存使用。
bad code:
1 | func get() []byte { |
为了避免这个陷阱,你需要从临时的slice中拷贝数据(而不是重新划分slice)
1 | func get() []byte { |
slice数据被破坏
slice重新划分后,引用的是原来的数组,对新slice的修改,会影响到原数据。
1 | base := []int{1, 2, 3, 4} |
复合字面量的多行表示时缺少逗号
复合字面量(数组、切片、字典、结构体)最后元素不跟上}
需要加上,
。
1 | _ = []int{1, 2} |
更新map的值
如果你有一个struct值的map,你无法更新单个的struct值。
1 | type data struct { |
good code:
1 | v := m["x"] |
控制
跳出多层循环
1 | loop: |
for中的迭代变量和闭包for
语句中的迭代变量在每次迭代时被重新使用。这就意味着你在 for
循环中创建的闭包(即函数字面量)将会引用同一个变量(而在那些goroutine开始执行时就会得到那个变量的值)
1 | data := []int{1, 2, 3} |
修正:
1 | data := []int{1, 2, 3} |
switch语句中的Fallthrough
不同于其他语言,go中的switch中的每个case语句执行完后默认终止,而不需要break来终止。若希望继续执行下一个case,加上fallthrough
1 | isSpace := func(ch byte) bool { |
修正:
1 | switch(ch) { |
go中的一个case中允许多个表达式,所以还可以这么写:
1 | switch(ch) { |
函数
函数参数的数组是值传递
函数调用参数的数组是对原数组的拷贝,函数中修改数组,不影响原数组。
1 | x := [3]int{1,2,3} |
可以使用数组指针或者切片实现函数中修改原数组
defer函数调用参数的求值
被defer的函数的参数会在defer声明时求值,而不是在函数实际执行时。
1 | var i int = 1 |
通道
关闭的channel的读写
对关闭的channel,读操作得到零值;写操作触发异常
nil channel的读写
读写nil channel永远阻塞
1 | var ch chan int |
1 | var ch chan int |
输出:
1 | fatal error: all goroutines are asleep - deadlock! |
并发
应用退出时协程未结束
应用程序退出时并不会等待线程完成结束。
错误:
1 | workFun := func(id int) { |
修正:
1 | var wg sync.WaitGroup |
阻塞的Goroutine和资源泄露
1 | func First(query string, replicas ...Search) Result { |
函数启动多工作流并发进行搜索功能,第一个搜索成功的协程将结果发送到结果通道c处。那其他协程呢?将会阻塞而导致资源的泄露。
不错的解决方法是使用一个有 default
情况的 select
语句和一个保存一个缓存结果的channel。 default
情况保证了即使当结果channel无法收到消息的情况下,goroutine也不会堵塞。
1 | func First(query string, replicas ...Search) Result { |
协程调度
有可能会出现这种情况,一个无耻的goroutine阻止其他goroutine运行。当你有一个不让调度器运行的 for
循环时,这就会发生。
1 | done := false |
只要for循环中没有会触发调度的代码,主协程就永远占用CPU而不让其他协程被调度。该例子在单核下会死循环,若在多核机子上运行,可以添加runtime.GOMAXPROCS(1)
来限制工作数。
HTTP
关闭HTTP响应
使用标准http库发起请求时,得到一个httpx响应变量。处理完后需要关闭响应body,即使没有读取它。还有需要注意的是关闭的地方。如:
1 | resp, err := http.Get("https://api.ipify.org?format=json") |
大多情况下,HTTP响应失败时,resp变量是nil,err是non-nil。在1处关闭会引发runtime panic。
参考资料
http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/