技巧分享:多 Goroutine 如何优雅处理错误?

开发 后端
在 Go 语言中 goroutine 是非常常用的一种方法,为此我们需要更了解 goroutine 配套的上下游(像是 context、error 处理等),应该如何用什么来保证。

[[409241]]

本文转载自微信公众号「脑子进煎鱼了」,作者陈煎鱼。转载本文请联系脑子进煎鱼了公众号。

大家好,我是煎鱼。

在 Go 语言中,goroutine 的使用是非常频繁的,因此在日常编码的时候我们会遇到一个问题,那就是 goroutine 里面的错误处理,怎么做比较好?

这是来自我读者群的问题。作为一个宠粉煎鱼,我默默记下了这个技术话题。今天煎鱼就大家来看看多 goroutine 的错误处理机制也有哪些!

一般来讲,我们的业务代码会是:

  1. func main() { 
  2.  var wg sync.WaitGroup 
  3.  wg.Add(2) 
  4.  go func() { 
  5.   log.Println("脑子进煎鱼了"
  6.   wg.Done() 
  7.  }() 
  8.  go func() { 
  9.   log.Println("煎鱼想报错..."
  10.   wg.Done() 
  11.  }() 
  12.  
  13.  time.Sleep(time.Second

在上述代码中,我们运行了多个 goroutine。但我想抛出 error 的错误信息出来,似乎没什么好办法...

通过错误日志记录

为此,业务代码中常见的第一种方法:通过把错误记录写入日志文件中,再结合相关的 logtail 进行采集和梳理。

但这又会引入新的问题,那就是调用错误日志的方法写的到处都是。代码结构也比较乱,不直观。

最重要的是无法针对 error 做特定的逻辑处理和流转。

利用 channel 传输

这时候大家可能会想到 Go 的经典哲学:不要通过共享内存来通信,而是通过通信来实现内存共享(Do not communicate by sharing memory; instead, share memory by communicating)。

第二种的方法:利用 channel 来传输多个 goroutine 中的 errors:

  1. func main() { 
  2.  gerrors := make(chan error) 
  3.  wgDone := make(chan bool) 
  4.  
  5.  var wg sync.WaitGroup 
  6.  wg.Add(2) 
  7.  
  8.  go func() { 
  9.   wg.Done() 
  10.  }() 
  11.  go func() { 
  12.   err := returnError() 
  13.   if err != nil { 
  14.    gerrors <- err 
  15.   } 
  16.   wg.Done() 
  17.  }() 
  18.  
  19.  go func() { 
  20.   wg.Wait() 
  21.   close(wgDone) 
  22.  }() 
  23.  
  24.  select { 
  25.  case <-wgDone: 
  26.   break 
  27.  case err := <-gerrors: 
  28.   close(gerrors) 
  29.   fmt.Println(err) 
  30.  } 
  31.  
  32.  time.Sleep(time.Second
  33.  
  34. func returnError() error { 
  35.  return errors.New("煎鱼报错了..."

输出结果:

  1. 煎鱼报错了... 

虽然使用 channel 后已经方便了不少。但自己编写 channel 总是需要关心一些非业务向的逻辑。

借助 sync/errgroup

因此第三种方法,就是使用官方提供的 sync/errgroup 标准库:

  1. type Group 
  2.     func WithContext(ctx context.Context) (*Group, context.Context) 
  3.     func (g *Group) Go(f func() error) 
  4.     func (g *Group) Wait() error 
  • Go:启动一个协程,在新的 goroutine 中调用给定的函数。
  • Wait:等待协程结束,直到来自 Go 方法的所有函数调用都返回,然后返回其中的第一个非零错误(如果有的话)。

结合其特性能够非常便捷的针对多 goroutine 进行错误处理:

  1. func main() { 
  2.  g := new(errgroup.Group
  3.  var urls = []string{ 
  4.   "http://www.golang.org/"
  5.   "https://golang2.eddycjy.com/"
  6.   "https://eddycjy.com/"
  7.  } 
  8.  for _, url := range urls { 
  9.   url := url 
  10.   g.Go(func() error { 
  11.    resp, err := http.Get(url) 
  12.    if err == nil { 
  13.     resp.Body.Close() 
  14.    } 
  15.    return err 
  16.   }) 
  17.  } 
  18.  if err := g.Wait(); err == nil { 
  19.   fmt.Println("Successfully fetched all URLs."
  20.  } else { 
  21.   fmt.Printf("Errors: %+v", err) 
  22.  } 

在上述代码中,其表现的是爬虫的案例。每一个计划新起的 goroutine 都直接使用 Group.Go 方法。在等待和错误上,直接调用 Group.Wait 方法就可以了。

使用标准库 sync/errgroup 这种方法的好处就是不需要关注非业务逻辑的控制代码,比较省心省力。

进阶使用

在真实的工程代码中,我们还可以基于 sync/errgroup 实现一个 http server 的启动和关闭 ,以及 linux signal 信号的注册和处理。以此保证能够实现一个 http server 退出,全部注销退出。

参考代码(@via 毛老师)如下:

  1. func main() { 
  2.  g, ctx := errgroup.WithContext(context.Background()) 
  3.  svr := http.NewServer() 
  4.  // http server 
  5.  g.Go(func() error { 
  6.   fmt.Println("http"
  7.   go func() { 
  8.    <-ctx.Done() 
  9.    fmt.Println("http ctx done"
  10.    svr.Shutdown(context.TODO()) 
  11.   }() 
  12.   return svr.Start() 
  13.  }) 
  14.  
  15.  // signal 
  16.  g.Go(func() error { 
  17.   exitSignals := []os.Signal{os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGINT} // SIGTERM is POSIX specific 
  18.   sig := make(chan os.Signal, len(exitSignals)) 
  19.   signal.Notify(sig, exitSignals...) 
  20.   for { 
  21.    fmt.Println("signal"
  22.    select { 
  23.    case <-ctx.Done(): 
  24.     fmt.Println("signal ctx done"
  25.     return ctx.Err() 
  26.    case <-sig: 
  27.     // do something 
  28.     return nil 
  29.    } 
  30.   } 
  31.  }) 
  32.  
  33.  // inject error 
  34.  g.Go(func() error { 
  35.   fmt.Println("inject"
  36.   time.Sleep(time.Second
  37.   fmt.Println("inject finish"
  38.   return errors.New("inject error"
  39.  }) 
  40.  
  41.  err := g.Wait() // first error return 
  42.  fmt.Println(err) 

内部基础框架有非常有这种代码,有兴趣的可以自己模仿着写一遍,收货会很多。

总结

在 Go 语言中 goroutine 是非常常用的一种方法,为此我们需要更了解 goroutine 配套的上下游(像是 context、error 处理等),应该如何用什么来保证。

 

再在团队中形成一定的共识和规范,这么工程代码阅读起来就会比较的舒适,一些很坑的隐藏 BUG 也会少很多 :)

 

责任编辑:武晓燕 来源: 脑子进煎鱼了
相关推荐

2021-08-10 07:41:24

ContextWaitGroupGoroutine

2024-01-15 08:09:44

Fluent错误代码

2022-02-15 08:38:04

错误逻辑异常编程程序

2023-10-10 13:23:18

空指针异常Java

2021-09-08 09:41:09

开发Go代码

2021-06-17 09:32:39

重复请求并发请求Java

2017-12-14 14:17:08

Windows使用技巧手册

2022-03-25 06:26:57

WiFi密码路由器

2023-05-12 12:09:38

职责链模式客服

2021-04-19 07:41:37

AcceptEmfile问题

2023-11-06 09:32:52

Java实践

2021-06-09 07:15:20

Go枚举技巧

2022-08-03 07:07:10

Spring数据封装框架

2021-12-07 05:53:36

IDMWindows 运维

2022-05-10 08:17:03

goroutine泄漏

2022-05-06 08:00:51

Golang编程语言Java

2021-09-27 23:28:29

Go多协程并发

2019-01-24 16:11:19

前端全局异常数据校验

2019-03-14 15:59:44

前端开发编程

2009-12-15 10:23:23

Ruby应用技巧
点赞
收藏

51CTO技术栈公众号