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

目录
相关文章
|
1月前
|
存储 负载均衡 监控
如何利用Go语言的高效性、并发支持、简洁性和跨平台性等优势,通过合理设计架构、实现负载均衡、构建容错机制、建立监控体系、优化数据存储及实施服务治理等步骤,打造稳定可靠的服务架构。
在数字化时代,构建高可靠性服务架构至关重要。本文探讨了如何利用Go语言的高效性、并发支持、简洁性和跨平台性等优势,通过合理设计架构、实现负载均衡、构建容错机制、建立监控体系、优化数据存储及实施服务治理等步骤,打造稳定可靠的服务架构。
35 1
|
1月前
|
Go 调度 开发者
探索Go语言中的并发模式:goroutine与channel
在本文中,我们将深入探讨Go语言中的核心并发特性——goroutine和channel。不同于传统的并发模型,Go语言的并发机制以其简洁性和高效性著称。本文将通过实际代码示例,展示如何利用goroutine实现轻量级的并发执行,以及如何通过channel安全地在goroutine之间传递数据。摘要部分将概述这些概念,并提示读者本文将提供哪些具体的技术洞见。
|
1月前
|
JSON 安全 网络协议
go语言使用内置函数和标准库
【10月更文挑战第18天】
21 3
|
2月前
|
Java 大数据 Go
Go语言:高效并发的编程新星
【10月更文挑战第21】Go语言:高效并发的编程新星
53 7
|
1月前
|
并行计算 安全 Go
Go语言的并发特性
【10月更文挑战第26天】Go语言的并发特性
19 1
|
2月前
|
安全 Go 调度
探索Go语言的并发模式:协程与通道的协同作用
Go语言以其并发能力闻名于世,而协程(goroutine)和通道(channel)是实现并发的两大利器。本文将深入了解Go语言中协程的轻量级特性,探讨如何利用通道进行协程间的安全通信,并通过实际案例演示如何将这两者结合起来,构建高效且可靠的并发系统。
|
2月前
|
安全 Go 开发者
破译Go语言中的并发模式:从入门到精通
在这篇技术性文章中,我们将跳过常规的摘要模式,直接带你进入Go语言的并发世界。你将不会看到枯燥的介绍,而是一段代码的旅程,从Go的并发基础构建块(goroutine和channel)开始,到高级模式的实践应用,我们共同探索如何高效地使用Go来处理并发任务。准备好,让Go带你飞。
|
2月前
|
安全 Go 调度
探索Go语言的并发之美:goroutine与channel
在这个快节奏的技术时代,Go语言以其简洁的语法和强大的并发能力脱颖而出。本文将带你深入Go语言的并发机制,探索goroutine的轻量级特性和channel的同步通信能力,让你在高并发场景下也能游刃有余。
|
3月前
|
存储 安全 Go
Go to Learn Go之并发
Go to Learn Go之并发
33 8
|
3月前
|
Go
Go to Learn Go之错误处理
Go to Learn Go之错误处理
51 7