我在使用 Go 过程中犯过的低级错误

2023-03-18 14:42:26 浏览数 (2)

循环中引用迭代器变量

循环迭代器变量是一个在每次循环迭代中采用不同值的单个变量。如果我们一直使用一个变量,可能会导致不可预知的行为。

代码语言: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/

0 人点赞