Go函数并发情况的错误处理

简介: Go函数并发情况的错误处理

前言


最近遇到了一个很有意思的问题, 感觉值得写一篇博客来记录一下, 也在大家遇到这种问题的时候可以有个参考;


下面这段代码大家都不陌生吧, 一个简单的多go程处理, 大家可以看看有没有什么问题

func handle() {
  var (
    errCh   = make(chan error, 1)
    doneCh  = make(chan struct{})
    records = make([]string, 100)
    gp      = gopool.NewGoPool(10) // go程池, 只允许开10个go程
  )
  for _, v := range records {
    if len(errCh) > 0 {
      break
    }
    gp.Add()
    go func(v string) {
      defer gp.Done()
      // handles
      // check(err)
      errCh <- fmt.Errorf("err")
    }(v)
  }
  go func() {
    gp.Wait()
    doneCh <- struct{}{}
  }()
  select {
  case <-doneCh:
    fmt.Println("done")
  case <-errCh:
    fmt.Println("err")
  }
}


问题


其实这段代码里有很大的安全隐患, 列举一下:

  1. 如果 for循环不判断 errCh, 在100个循环完之前产生11个err, errCh插入不进去, for循环就会直接死锁, 走不到下面的select;
  2. 虽然判断了errCh保证可以终止for循环, 但是其他go程产生错误也会死锁, 并且每次运行这个函数都有可能产生死锁go程, 会产生 Goroutine Leak;
  3. 最后select不能完全保证doneCh和errCh 哪个优先监听到;


那么有问题就要有对应的解决思路及方案, 下面给出几个:

方案一 扩充errCh的大小;

点评

最简单粗暴的方案, 但是会造成内存浪费;

实现

errCh   = make(chan error, len(records))


方案二 使用context.WithCancel;

点评

context替换errCh作为监听err, cancel() 可以多次执行, 不会阻塞;

比思路一优雅一些, 但是代码会比较冗余;

实现

func handle() {
  var (
    ctx, cancel = context.WithCancel(context.Background())
    doneCh      = make(chan struct{})
    records     = make([]string, 100)
    gp          = gopool.NewGoPool(10) // go程池, 只允许开10个go程
  )
  go func() {
    for _, v := range records {
      select {
      case <-ctx.Done():
        return
      default:
      }
      gp.Add()
      go func(v string) {
        defer gp.Done()
        // handles
        // check(err)
        cancel()
      }(v)
    }
  }()
  go func() {
    gp.Wait()
    time.Sleep(1 * time.Millisecond)
    select {
    case <-ctx.Done():
      return
    default:
    }
    doneCh <- struct{}{}
  }()
  select {
  case <-doneCh:
    fmt.Println("done")
  case <-ctx.Done():
    fmt.Println("err")
  }
}

方案三 使用sync.Once

点评

使用 Once 来限制 插入errCh操作只执行一次;

目前最优雅的思路, 代码改动也最少;

实现

func handle() {
  var (
    errCh   = make(chan error, 1)
    doneCh  = make(chan struct{})
    records = make([]string, 100)
    gp      = gopool.NewGoPool(10) // go程池, 只允许开10个go程
    _once   = new(sync.Once)
  )
  for _, v := range records {
    if len(errCh) > 0 {
      break
    }
    gp.Add()
    go func(v string) {
      defer gp.Done()
      // handles
      // check(err)
      _once.Do(func() {
        errCh <- fmt.Errorf("err")
      })
    }(v)
  }
  go func() {
    gp.Wait()
    time.Sleep(1 * time.Millisecond) // 优先监听errCh
    doneCh <- struct{}{}
  }()
  select {
  case <-doneCh:
    fmt.Println("done")
  case <-errCh:
    fmt.Println("err")
  }
}

结束语


大家如果有更好的思路/方案可以在评论区/私信给我, 共同学习共同进步;


我自己写了一个Go开箱即用的开源项目, 里面封装了常用的一些组件, git clone下来就可以直接进行API开发, 有兴趣的可以给个Star, 会一直持续维护;


项目地址 https://github.com/shixiaofeia/fly

目录
相关文章
|
7月前
|
人工智能 安全 算法
Go入门实战:并发模式的使用
本文详细探讨了Go语言的并发模式,包括Goroutine、Channel、Mutex和WaitGroup等核心概念。通过具体代码实例与详细解释,介绍了这些模式的原理及应用。同时分析了未来发展趋势与挑战,如更高效的并发控制、更好的并发安全及性能优化。Go语言凭借其优秀的并发性能,在现代编程中备受青睐。
244 33
|
2月前
|
存储 安全 Java
【Golang】(4)Go里面的指针如何?函数与方法怎么不一样?带你了解Go不同于其他高级语言的语法
结构体可以存储一组不同类型的数据,是一种符合类型。Go抛弃了类与继承,同时也抛弃了构造方法,刻意弱化了面向对象的功能,Go并非是一个传统OOP的语言,但是Go依旧有着OOP的影子,通过结构体和方法也可以模拟出一个类。
206 1
|
3月前
|
存储 Java Go
对比Java学习Go——函数、集合和OOP
Go语言的函数支持声明与调用,具备多返回值、命名返回值等特性,结合`func`关键字与类型后置语法,使函数定义简洁直观。函数可作为一等公民传递、赋值或作为参数,支持匿名函数与闭包。Go通过组合与接口实现面向对象编程,结构体定义数据,方法定义行为,接口实现多态,体现了Go语言的简洁与高效设计。
|
6月前
|
存储 Go 开发者
Go 语言中如何处理并发错误
在 Go 语言中,并发编程中的错误处理尤为复杂。本文介绍了几种常见的并发错误处理方法,包括 panic 的作用范围、使用 channel 收集错误与结果,以及使用 errgroup 包统一管理错误和取消任务,帮助开发者编写更健壮的并发程序。
155 4
Go 语言中如何处理并发错误
|
4月前
|
数据采集 Go API
Go语言实战案例:多协程并发下载网页内容
本文是《Go语言100个实战案例 · 网络与并发篇》第6篇,讲解如何使用 Goroutine 和 Channel 实现多协程并发抓取网页内容,提升网络请求效率。通过实战掌握高并发编程技巧,构建爬虫、内容聚合器等工具,涵盖 WaitGroup、超时控制、错误处理等核心知识点。
|
4月前
|
数据采集 消息中间件 编解码
Go语言实战案例:使用 Goroutine 并发打印
本文通过简单案例讲解 Go 语言核心并发模型 Goroutine,涵盖协程启动、输出控制、主程序退出机制,并结合 sync.WaitGroup 实现并发任务同步,帮助理解 Go 并发设计思想与实际应用。
|
6月前
|
人工智能 Dart Go
Go语言中的make和new函数的区别及使用场景
本文详细解析了Go语言中`make`和`new`函数的使用方法及区别。`make`用于创建切片、映射和通道等引用类型,返回初始化后的值;`new`用于创建任意类型的零值对象,返回指向该对象的指针。文章通过多个示例说明两者的应用场景,并总结了面试中可能遇到的相关问题,如底层实现、使用场景及优缺点等,帮助读者更好地理解和区分这两个函数。
212 1
|
7月前
|
Go 调度
GO语言函数的内部运行机制分析
以上就是Go语言中函数的内部运行机制的概述,展示了函数在Go语言编程中如何发挥作用,以及Go如何使用简洁高效的设计,使得代码更简单,更有逻辑性,更易于理解和维护。尽管这些内容深入了一些底层的概念,但我希望通过这种方式,将这些理论知识更生动、更形象地带给你,让你在理解的同时找到编程的乐趣。
158 5
|
7月前
|
Go Python
函数的定义与调用 -《Go语言实战指南》
本文介绍了 Go 语言中函数的核心特性与用法,包括基本定义格式、调用方式、多返回值、返回值命名、参数类型简写、可变参数、高阶函数及匿名函数等内容。通过示例代码详细展示了如何定义和使用不同类型的函数,使读者能够全面了解 Go 函数的灵活性与强大功能。
147 12
|
8月前
|
数据采集 监控 Go
用 Go 实现一个轻量级并发任务调度器(支持限速)
本文介绍了如何用 Go 实现一个轻量级的并发任务调度器,解决日常开发中批量任务处理的需求。调度器支持最大并发数控制、速率限制、失败重试及结果收集等功能。通过示例代码展示了其使用方法,并分析了核心组件设计,包括任务(Task)和调度器(Scheduler)。该工具适用于网络爬虫、批量请求等场景。文章最后总结了 Go 并发模型的优势,并提出了扩展功能的方向,如失败回调、超时控制等,欢迎读者交流改进。
336 25