• 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 做任务编排的场景,看一下好像没啥问题,运行看看结果。

    图片

    为啥不是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 并不能捕获,程序会直接挂掉。 图片

    但是你总不能每次开启一个新的 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

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

    一个往已关闭的 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 在日常开发中容易犯下的错。 当然还远远不止这些,你可以在下方留言中补充你犯过的错。

  • 相关阅读:
    Python学习 之 文件
    Python学习 之 对内存的使用(浅拷贝和深拷贝)
    Python学习 之 爬虫
    Python学习 之 正则表达式
    为何现在的网页广告都是有关你搜索或者购买过的商品 2015-08-22 22:06 1534人阅读 评论(35) 收藏
    Junit使用注意点
    用递归方式在JSON中查找对象
    利用StringBuffer来替换内容
    使用ant时 出现 java.lang.OutOfMemoryErro r: Java heap space的解决办法
    python-re使用举例
  • 原文地址:https://www.cnblogs.com/cheyunhua/p/16193824.html
Copyright © 2020-2023  润新知