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

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

(0)

相关推荐

  • recover.panic.defer.2021.03.03

    Defer, Panic, and Recover 在 Go 语言中,recover 和 panic 的关系是什么? 我们先看一个基础的例子,在 main 方法体中启动一个协程,在协程内部主动调用 p ...

  • Go 最细节篇 — chan 为啥没有判断 close 的接口 ?

    大纲 Go 为什么没有判断 close 的接口? Go 关闭 channel 究竟做了什么? `closechan` 一个判断 chan 是否 close 的函数 思考方法一:通过"写&qu ...

  • RabbitMQ Golang教程(二)

    RabbitMQ Golang教程(二) 任务队列 什么是任务队列 ? 把要执行的任务放在队列中.使用较多的任务队列有machiney.Celery.goWorker.YTask.每一个任务队列都有自 ...

  • 手把手教姐姐写消息队列

    前言 这周姐姐入职了新公司,老板想探探他的底,看了一眼他的简历,呦呵,精通kafka,这小姑娘有两下子,既然这样,那你写一个消息队列吧.因为要用go语言写,这可给姐姐愁坏了.赶紧来求助我,我这么坚贞不 ...

  • 22 Go常见的并发模式和并发模型

    一 Go并发模型 传统的编程语言C++ Java Python等,他们的并发逻辑多事基于操作系统的线程.并发执行单元(线程)之间的通信利用的就是操作系统提供的线程或进程间通信的原语.如:共享内存.信号 ...

  • 工程项目结算中常犯的一些错误,值得一看!

    作者:胡跃 工程结算资料性质等同于我们进餐馆用餐后接到的结账单或超市出口扫码后打印出的小票.只是工程结算资料相比餐馆.超市里的结账单据出现错误的种类和数量要多一些,金额要大一些.关于工程结算资料出现错 ...

  • 家长签字期末通知书评语你在无意识中会犯一些小错误,但总的来说还是一个遵规守纪的学生,

    家长签字期末通知书评语 21.你在无意识中会犯一些小错误,但总的来说还是一个遵规守纪的学生,以后应注意培养自己的控制能力.学习上你有一定的自觉性,不过努力程度恐怕还不够,希望你今后能专心听讲,大胆发问 ...

  • 【生活中常犯三个错误】向糊涂人,说了明白...

    [生活中常犯三个错误] 向糊涂人,说了明白话: 和不靠谱的人做正经事: 和无情的人谈感情. 每个人的心里要有杆秤,秤自己,也秤别人玩.话,要和明白人说:事,要与踏实人做:情,要同厚道人谈.

  • 【京剧知识】京剧演唱中容易犯的十个错误

    一.吃字 戏曲演员再唱念上,讲究口齿清楚,这样才能吐字真切,发音准确,把唱词或话白送入观众耳中.'吃字'即为咬字不清,犹如把字吃到肚子里一样.演员导致'吃字'的原因在于不能够正确的运用唇,齿,舌,牙, ...

  • 干货:练习杨氏太极拳中容易犯的几类错误

    李元芝(杨雅芝),1970年出生,浙江义乌人,主要习练杨氏太极拳103式.杨氏太极剑67式.杨氏太极刀十三刀及杨氏太极推手.2017年3月被杨氏太极拳第五代嫡传人杨军老师收为弟子,先后参加过杨军老师的 ...

  • 简放:在趋势中,犯错误的概率会低很多(20210525)

    昨天市场延续震荡格局,上午指数下探后拉升翻红,创业板指数收长下影锤头线,显示出低位有资金承接.整体来看,市场走势还是不错的. 还是那句话,大盘看不出什么风险,上证指数就是一个区间的震荡,而最强的创业板 ...

  • 人非圣贤孰能无过?这篇文章告诉你如何正确面对生活中所犯的错误

    音频版课程链接,让你解放双手,边走边听: https://www.ximalaya.com/jiaoyu/20463813/226190482 凡事都有解 导语:你犯过错吗? 答案显而易见,人非圣贤孰 ...

  • 交易中常犯的错误,该如何规避?

    前面和一位认识了很多年的交易同事聊天,我们讨论了交易的很多层面,包括长线交易与短线交易的异同点等,因为他是擅长短线交易而我则是信奉中长线投资的.全部聊下来之后令我最受启发的居然是他提出的一个问题. 他 ...

  • 你在经营企业中犯过这些错误吗?

    一个 曾经失败欠债2.5亿 ,如今又东山再起 坐拥580亿资产 的退休企业家向你 讲述他的创业历程和 营销心得 你在创业的时候会遇到资金紧张,运营困难以及其他一些经常让你感到困惑和痛苦的问题吗?以至于 ...