在Go中,你犯过这些错误吗

简介: 在Go中,你犯过这些错误吗

迭代器变量上使用 goroutine


这算高频吧。


package main
import (
  "fmt"
  "sync"
)
func main() {
  var wg sync.WaitGroup
  items := []int{1, 2, 3, 4, 5}
  for index, _ := range items {
    wg.Add(1)
    go func() {
      defer wg.Done()
      fmt.Printf("item:%v\\n", items[index])
    }()
  }
  wg.Wait()
}


一个很简单的利用 sync.waitGroup 做任务编排的场景,看一下好像没啥问题,运行看看结果。

1668504513300.jpg

为啥不是1-5(当然不是顺序的)。


原因很简单,循环器中的 i 实际上是一个单变量,go func 里的闭包只绑定在一个变量上, 每个 goroutine 可能要等到循环结束才真正的运行,这时候运行的 i 值大概率就是5了。没人能保证这个过程,有的只是手段。

正确的做法,


func main() {
  var wg sync.WaitGroup
  items := []int{1, 2, 3, 4, 5}
  for index, _ := range items {
    wg.Add(1)
    go func(i int) {
      defer wg.Done()
      fmt.Printf("item:%v\\n", items[i])
    }(index)
  }
  wg.Wait()
}

通过将i作为一个参数传入闭包中,i 每次迭代都会被求值, 并放置在goroutine的堆栈中,因此每个切片元素最终都会被执行打印。

或者这样,


for index, _ := range items {
    wg.Add(1)
    i:=index
    go func() {
      defer wg.Done()
      fmt.Printf("item:%v\\n", items[i])
    }()
  }


WaitGroup


上面的例子有用到sync.waitGroup使用不当,也会犯错

我把上面的例子稍微改动复杂一点点。


package main
import (
  "errors"
  "github.com/prometheus/common/log"
  "sync"
)
type User struct {
  userId int
}
func main() {
  var userList []User
  for i := 0; i < 10; i++ {
    userList = append(userList, User{userId: i})
  }
  var wg sync.WaitGroup
  for i, _ := range userList {
    wg.Add(1)
    go func(item int) {
      _, err := Do(userList[item])
      if err != nil {
        log.Infof("err message:%v\\n", err)
        return
      }
      wg.Done()
    }(i)
  }
  wg.Wait()
  // 处理其他事务
}
func Do(user User) (string, error) {
  // 处理杂七杂八的业务....
  if user.userId == 9 {
    // 此人是非法用户
    return "失败", errors.New("非法用户")
  }
  return "成功", nil
}


发现问题严重性了吗?

当用户id等于9的时候err !=nil直接return 了,导致waitGroup计数器根本没机会减1最终 wait会阻塞,多么可怕的bug

在绝大多数的场景下,我们都必须这样:


func main() {
  var userList []User
  for i := 0; i < 10; i++ {
    userList = append(userList, User{userId: i})
  }
  var wg sync.WaitGroup
  for i, _ := range userList {
    wg.Add(1)
    go func(item int) {
      defer wg.Done() //重点
      //....业务代码
      //....业务代码
      _, err := Do(userList[item])
      if err != nil {
        log.Infof("err message:%v\n", err)
        return
      }
    }(i)
  }
  wg.Wait()
}


野生 goroutine


我不知道你们公司是咋么处理异步操作的,是下面这样吗?


func main() {
  // doSomething
  go func() {
    // doSomething
  }()
}

我们为了防止程序中出现不可预知的panic导致程序直接挂掉,都会加入 recover

func main() {
  defer func() {
    if err := recover(); err != nil {
      fmt.Printf("%v\n", err)
    }
  }()
  panic("处理失败")
}

但是如果这时候我们直接开启一个 goroutine在这个goroutine 里面发生了panic

func main() {
  defer func() {
    if err := recover(); err != nil {
      fmt.Printf("%v\n", err)
    }
  }()
  go func() {
    panic("处理失败")
  }()
  time.Sleep(2 * time.Second)
}

此时最外层的recover并不能捕获,程序会直接挂掉

1668504550634.jpg

但是你总不能每次开启一个新的goroutine就在里面recover,

func main() {
  defer func() {
    if err := recover(); err != nil {
      fmt.Printf("%v\n", err)
    }
  }()
  // func1
  go func() {
    defer func() {
      if err := recover(); err != nil {
        fmt.Printf("%v\n", err)
      }
    }()
    panic("错误失败")
  }()
  // func2
  go func() {
    defer func() {
      if err := recover(); err != nil {
        fmt.Printf("%v\n", err)
      }
    }()
    panic("请求错误")
  }()
  time.Sleep(2 * time.Second)
}


多蠢啊。所以基本上大家都会包一层。


package main
import (
  "fmt"
  "time"
)
func main() {
  defer func() {
    if err := recover(); err != nil {
      fmt.Printf("%v\n", err)
    }
  }()
  // func1
  Go(func() {
    panic("错误失败")
  })
  // func2
  Go(func() {
    panic("请求错误")
  })
  time.Sleep(2 * time.Second)
}
func Go(fn func()) {
  go RunSafe(fn)
}
func RunSafe(fn func()) {
  defer func() {
    if err := recover(); err != nil {
      fmt.Printf("错误:%v\n", err)
    }
  }()
  fn()
}


当然我这里只是简单都打印一些日志信息,一般还会带上堆栈都信息。


channel


channelgo中的地位实在太高了,各大开源项目到处都是channel的影子, 以至于你在工业级的项目 issues 中搜索channel能看到很多的bug, 比如 etcd 这个 issue,

1668504580364.jpg

一个往已关闭的channel中发送数据引发的panic,等等类似场景很多

这个故事告诉我们,否管大不大佬,改写的bug还是会写,手动狗头

channel除了上述高频出现的错误,还有以下几点:


直接关闭一个 nil 值 channel 会引发 panic


package main
func main() {
  var ch chan struct{}
  close(ch)
}


关闭一个已关闭的 channel 会引发 panic。


package main
func main() {
  ch := make(chan struct{})
  close(ch)
  close(ch)
}

另外,有时候使用channel不小心会导致goroutine泄露,比如下面这种情况,

package main
import (
  "context"
  "fmt"
  "time"
)
func main() {
  ch := make(chan struct{})
  cx, _ := context.WithTimeout(context.Background(), time.Second)
  go func() {
    time.Sleep(2 * time.Second)
    ch <- struct{}{}
    fmt.Println("goroutine 结束")
  }()
  select {
  case <-ch:
    fmt.Println("res")
  case <-cx.Done():
    fmt.Println("timeout")
  }
  time.Sleep(5 * time.Second)
}

启动一个goroutine去处理业务,业务需要执行2秒而我们设置的超时时间是1秒。 这就会导致channel从未被读取, 我们知道没有缓冲的channel必须等发送方和接收方都准备好才能操作。 此时 goroutine会被永久阻塞在ch <- struct{}{}这行代码,除非程序结束。 而这就是goroutine 泄露

解决这个也很简单,把无缓冲的channel改成缓冲为1


总结


这篇文章主要介绍了使用Go在日常开发中容易犯下的错。 当然还远远不止这些,你可以在下方留言中补充你犯过的错。

另外之前一直没有自己的 blog,目前搭了一个,网址:https://www.syst.top,有换友链的下方留个言,尽管我还没友链页面

相关文章
|
7月前
|
存储 Java 测试技术
100 个 Go 错误以及如何避免:1~4
100 个 Go 错误以及如何避免:1~4
162 0
|
5月前
|
编译器 Go
Go中遇到的bug
【7月更文挑战第4天】
57 7
|
5月前
|
Go 开发工具 git
Go解决问题
【7月更文挑战第1天】
56 8
|
7月前
|
Java Linux Go
关于我想写一个Go系列的这件事
本文是Go语言专栏的开篇,作者sharkChili分享了他对Go语言的喜爱,并简要介绍了如何在Windows和Linux上搭建Go环境。文章包括下载安装包、解压、配置环境变量等步骤。此外,还展示了编写并运行第一个&quot;Hello, sharkChili&quot;的Go程序。最后提到了Go项目的`.gitignore`文件示例,并鼓励读者关注作者的公众号以获取更多Go语言相关的内容。
44 0
|
7月前
|
数据库连接 Go 数据库
【Go 语言专栏】Go 语言中的错误注入与防御编程
【4月更文挑战第30天】本文探讨了Go语言中的错误注入和防御编程。错误注入是故意引入错误以测试系统异常情况下的稳定性和容错性,包括模拟网络故障、数据库错误和手动触发错误。防御编程则强调编写代码时考虑并预防错误,确保程序面对异常时稳定运行。Go语言的错误处理机制包括多返回值和自定义错误类型。结合错误注入和防御编程,可以提升软件质量和可靠性,打造更健壮的系统。开发人员应重视这两方面,以实现更优质的软件产品。
70 0
|
7月前
|
Go 开发者 UED
Go错误处理方式真的不好吗?
Go错误处理方式真的不好吗?
39 0
|
7月前
|
Go
开心档之 Go 错误处理
开心档之 Go 错误处理
|
7月前
|
Go
100 个 Go 错误以及如何避免:9~12
100 个 Go 错误以及如何避免:9~12
401 0
|
7月前
|
测试技术 Go 调度
100 个 Go 错误以及如何避免:5~8
100 个 Go 错误以及如何避免:5~8
67 0
|
Java 测试技术 Go
送给学Go或者转Go同学的一套编码规范
有没有 xd 们是从别的语言转 Go
191 0
送给学Go或者转Go同学的一套编码规范