循环中引用迭代器变量
循环迭代器变量是一个在每次循环迭代中采用不同值的单个变量。如果我们一直使用一个变量,可能会导致不可预知的行为。
代码语言:javascript复制in := []int{1, 2, 3}
var out []*int
for _, v := range in {
out = append(out, &v)
}
fmt.Println("Values:", *out[0], *out[1], *out[2])
fmt.Println("Addresses:", out[0], out[1], out[2])
打印结果:
代码语言:javascript复制Values: 3 3 3
Addresses: 0xc00001e0e0 0xc00001e0e0 0xc00001e0e0
因为v是一个单一的值,但是每次迭代都有一个新的值并追加到out的切片中,这就不难解释都被最后一个元素覆盖了。
解决方式也很简单,只要加一个临时变量即可
代码语言:javascript复制in := []int{1, 2, 3}
var out []*int
for _, v := range in {
v := v
out = append(out, &v)
}
fmt.Println("Values:", *out[0], *out[1], *out[2])
fmt.Println("Addresses:", out[0], out[1], out[2])
输出:
代码语言:javascript复制Values: 1 2 3
Addresses: 0xc00019a088 0xc00019a090 0xc00019a098
循环中使用goroutine
如下函数可能最后输出的都是3
代码语言:javascript复制list := []int{1, 2, 3}
for _, v := range list {
go func() {
fmt.Printf("%d ", v)
}()
}
因为 goroutine 最终读取变量的时间是不确定的,从而 goroutine 中获取到变量的值不一定符合最初的预期。
解决方法也很简单,v 作为一个参数传入 goroutine 中,每个 v 都会被独立计算并保存到 goroutine 的栈中,从而得到预期的结果。
代码语言:javascript复制list := []int{1, 2, 3}
for _, v := range list {
go func(v int) {
fmt.Printf("%d ", v)
}(v)
}
或者另外一种在循环内定义新的变量。
代码语言:javascript复制list := []int{1, 2, 3}
for i := range list {
v := list[i]
go func() {
fmt.Printf("%d ", v)
}()
}
循环调用WaitGroup.Wait
这个错误可以使用WaitGroup类型的共享变量,如下面的代码所示,第7行的Wait()只有在第5行的Done()被调用len(tasks)次时才能解除阻塞,因为它被用作调用第2行的Add()的参数。然而,Wait()是在循环内调用的,所以它在接下来的迭代中会阻塞在第4行的Goroutine创建。简单的解决方案是将Wait()的调用从循环中移出。
代码语言:javascript复制var wg sync.WaitGroup
wg.Add(len(tasks))
for _, t := range tasks {
go func(t *task) {
defer group.Done()
}(t)
// group.Wait()
}
group.Wait()
在循环中使用 defer
defer在函数返回之前不会执行。defer除非您确定自己在做什么,否则不应在循环中使用defer。
代码语言:javascript复制var mutex sync.Mutex
type Person struct {
Age int
}
persons := make([]Person, 10)
for _, p := range persons {
mutex.Lock()
defer mutex.Unlock()
p.Age = 13
// mutex.Unlock()
}
在上面的例子中,如果你使用defer,下一次迭代不能持有互斥锁,因为锁已经被使用并且永远阻塞。
如果您真的需要在循环内使用 defer,您可能需要委托另一个函数来完成这项工作。
代码语言:javascript复制var mutex sync.Mutex
type Person struct {
Age int
}
persons := make([]Person, 10)
for _, p := range persons {
func() {
mutex.Lock()
defer mutex.Unlock()
p.Age = 13
}()
}
发送到无缓冲区的通道
你可以从一个Goroutine向通道发送数值,并接收这些数值到另一个Goroutine。默认情况下,发送和接收都是阻塞的,直到另一方准备好。这允许Goroutine在没有显式锁或条件变量的情况下进行同步。
代码语言:javascript复制func doReq(timeout time.Duration) obj {
// ch :=make(chan obj)
ch := make(chan obj, 1)
go func() {
obj := do()
ch <- result
} ()
select {
case result = <- ch :
return result
case<- time.After(timeout):
return nil
}
}
doReq函数在第4行创建了一个子Goroutine来处理一个请求,这是Go服务器程序中的一个常见做法。子Goroutine执行do函数,并在第6行通过ch通道将结果发回给父程序。子程序将在第6行阻塞,直到父程序在第9行收到来自ch的结果。同时,父程序将在select处阻塞,直到子程序向ch发送结果(第9行)或超时发生(第11行)。如果超时提前发生,父代将在第12行从doReq函数中返回,没有人可以再从ch那里接收结果,这导致子代永远被阻塞。修复方法是将ch从一个无缓冲的通道改为有缓冲的通道,这样子Goroutine就可以一直发送结果,即使父级已经退出。
另一个解决方法是在第6行使用一个带有空默认情况的选择语句,这样如果没有Goroutine收到ch,就会发生默认。尽管这个解决方案可能并不总是有效。
代码语言:javascript复制...
select {
case ch <- result:
default:
}
...
不使用 -race 选项
我经常见到的一个错误是在测试 go 应用的时候没有带 -race 选项。
正如这篇报告所描述的,虽然 Go 是 “旨在使并发编程变得更容易,更不易出错”,但实际上我们仍然会遭遇很多并发的问题。
显然,Go 的竞争检查 (race detector) 无法解决每一个并发问题,然而它依然是一个有价值的工具,我们应当确保在做测试的时候(go test) 始终使用它。
代码语言:javascript复制$ go test -race pkg // to test the package
$ go run -race pkg.go // to run the source file
$ go build -race // to build the package
$ go install -race pkg // to install the package
启用竞争检测器后,编译器将记录在代码中访问内存的时间和方式,同时runtime监视对共享变量的非同步访问。
当发现数据竞争时,竞争检测器会打印一份报告,其中包含冲突访问的堆栈跟踪。下面是一个例子:
代码语言:javascript复制WARNING: DATA RACE
Read by goroutine 185:
net.(*pollServer).AddFD()
src/net/fd_unix.go:89 0x398
net.(*pollServer).WaitWrite()
src/net/fd_unix.go:247 0x45
net.(*netFD).Write()
src/net/fd_unix.go:540 0x4d4
net.(*conn).Write()
src/net/net.go:129 0x101
net.func·060()
src/net/timeout_test.go:603 0xaf
Previous write by goroutine 184:
net.setWriteDeadline()
src/net/sockopt_posix.go:135 0xdf
net.setDeadline()
src/net/sockopt_posix.go:144 0x9c
net.(*conn).SetDeadline()
src/net/net.go:161 0xe3
net.func·061()
src/net/timeout_test.go:616 0x3ed
Goroutine 185 (running) created at:
net.func·061()
src/net/timeout_test.go:609 0x288
Goroutine 184 (running) created at:
net.TestProlongTimeout()
src/net/timeout_test.go:618 0x298
testing.tRunner()
src/testing/testing.go:301 0xe8
总结
如上是初学golang过程中经常出现的一些低级错误,从错误中学习,多看官方文档,从而避免错误。
参考
- https://learnku.com/articles/38669
- https://medium.com/swlh/5-mistakes-ive-made-in-go-75fb64b943b8
- https://haormj.xyz/post/use_goroutine_in_for/