golang中channel+error来做异步错误处理有多香

 更新时间:2023年01月19日 10:22:49   作者:文大侠666  
官方推荐golang中错误处理当做值处理, 既然是值那就可以在channel中传输,这篇文章主要介绍了golang 错误处理channel+error真的香,需要的朋友可以参考下

官方推荐golang中错误处理当做值处理, 既然是值那就可以在channel中传输,本文带你看看golang中channel+error来做异步错误处理有多香,看完本文还会觉得golang的错误处理相比java try catch一点优势都没有吗?

场景

如下,一次任务起多个协程异步处理任务,比如同时做服务/redis/mysql/kafka初始化,当某一个协程出现错误(初始化失败)时,程序是停止还是继续呢?如何记录错误?如何控制优雅的退出全部工作协程呢?

image.png

为了解决类似的问题,常见如下三种解决方案:

1.中断退出并记录日志

如下,最简单粗暴的方式就是,一旦协程中发生错误,记录日志立即退出,外层如果想取到错误可以通过共享全局变量,单个协程无法控制所有协程动作。

// 出错中断协程,打印日志,退出

func TestSimpleExit(t *testing.T) {
	wg := sync.WaitGroup{}

	wg.Add(1)
	go func() {
		defer wg.Done()

		//do something
		if err := doSomething(); err != nil {
			t.Logf("Error when call doSomething:%v\n", err)
			return
		}
	}()

	wg.Wait()
}

2.监控error,可选记录日志或退出

前面讲过,既然error是值,那就可以在channel中传输,可以单独开一个channel,所有协程错误都发送到这个通道。

	// 数据处理流程
	dataFunc := func(ctx context.Context, dataChan chan int, errChan chan error) {
		defer wg.Done()

		for {
			select {
			case v, ok := <-dataChan:
				if !ok {
					log.Println("Receive data channel close msg!")
					return
				}

				if err := doSomething2(v); err != nil {
					errChan <- err
					continue
				}

				// do ...

			case <-ctx.Done():
				log.Println("Receive exit msg!")
				return
			}
		}
	}

	wg.Add(1)
	go dataFunc(ctx, dataChan, errChan)

	wg.Add(1)
	go dataFunc(ctx, dataChan, errChan)

监控错误error通道,统一记录和退出,一旦检测到错误可以通过ctx通知所有协程退出,这里可以灵活控制监控到错误时的错误处理策略(是否记录日志/是否退出等),error通道可以同步或异步处理
整体流程如下

image.png

异步监控error

	// 错误处理流程,error处理通道异步等待
	wg.Add(1)
	go func(errChan chan error) {
		defer wg.Done()

		for {
			select {
			case v, ok := <-errChan:
				if !ok {
					log.Println("Receice err channel close msg!")
					return
				}

				// 收到错误时,可选择记录日志或退出
				if v != nil {
					t.Logf("Error when call doSomething:%v\n", v)
					cancel() // 通知全部退出
					return
				}

			case <-ctx.Done():
				log.Println("Receive exit msg!")
				return
			}
		}

	}(errChan)

	dataChan <- 1
	wg.Wait()

同步监控error

	// 错误处理流程,error处理通道同步等待
	for {
		select {
		case v, ok := <-errChan:
			if !ok {
				log.Println("Receice err channel close msg!")
				goto EXIT
			}

			// 收到错误时,可选择记录日志或退出
			if v != nil {
				t.Logf("Error when call doSomething:%v\n", v)
				cancel()
				goto EXIT
			}

		case <-ctx.Done():
			log.Println("Receive exit msg!")
			goto EXIT
		}
	}

EXIT:
	wg.Wait()

3.官方库errgroup

考虑到error输出到通道后统一处理是golang常用手段,官方也针对封装了一个error处理包,errgroup顾名思义,多个协程的error被当做一个组,一旦某个协程出错所有协程都退出,只输出第一个error

func TestSimpleChannel5(t *testing.T) {
	eg, ctx := errgroup.WithContext(context.Background())

	dataChan := make(chan int)
	defer close(dataChan)

	// 数据处理流程
	dataFunc := func() error {
		for {
			select {
			case v, ok := <-dataChan:
				if !ok {
					log.Println("Receive data channel close msg!")
					return nil
				}

				if err := doSomething2(v); err != nil {
					return err
				}

				// do ...

			// 增加ctx通知完成
			case <-ctx.Done():
				log.Println("Receive exit msg!")
				return nil
			}
		}
	}

	eg.Go(dataFunc)
	eg.Go(dataFunc)
	eg.Go(dataFunc)

	dataChan <- 1

	// 错误处理流程,任何一个协程出现error,则会调用ctx对应cancel函数,所有相关协程都会退出
	if err := eg.Wait(); err != nil {
		fmt.Printf("Something is wrong->%v\n", err)
	}
}

类似上一小节,可以看到errgroup就是结合waitgroup cancel和channel通道封装的

4.监控error,全部日志合并后输出

同样是上述场景,有时候我们的需求是返回所有的错误(不是第一个错误)。

  • 比如在服务启动时,对 redis、kafka、mysql 等各种资源初始化场景,可以把所有相关资源初始化的错误都返回,展示给用户统一排查。
  • 另一种场景就是在 web 请求中,校验请求参数时,返回所有参数的校验错误给客户端的场景。

这种需求,一般考虑使用多错误管理(hashicorp/go-multierror库),如下一个简答同步任务演示多错误管理,所有返回的错误可以通过Append归并成一个错误,实际上是通过error wrap的方式合并起来的,因此也可以使用Is/As判断嵌套error。

// 多路协程error合并,用于多路check场景
func TestSimpleChannel3(t *testing.T) {

	// 同步执行多个任务,返回error合并
	var err = func() error {
		var result error

		if err := doSomething(); err != nil {
			result = multierror.Append(result, err)
		}

		if err := doSomething2(nil); err != nil {
			result = multierror.Append(result, err)
		}

		return result
	}()

	// 打印输出
	if err != nil {
		fmt.Printf("%v\n", err)
	}

	// 获取错误列表
	if err != nil {
		if merr, ok := err.(*multierror.Error); ok {
			fmt.Printf("%v\n", merr.Errors)
		}
	}

	// 判断是否为某种类型
	if err != nil && errors.Is(err, Error1) {
		fmt.Println("Errors contain error 1")
	}

	// 判断是否其中一个error能够转换成指定error
	var e MyError
	if err != nil && errors.As(err, &e) {
		fmt.Println("One Error can be convert to nyerror")
	}
}

那么,在起多个异步任务时,就可以如下处理,返回的多个error通过channel消费合并展示。

func TestSimpleChannel4(t *testing.T) {
	wg := sync.WaitGroup{}

	taskNum := 10
	errChan := make(chan error, taskNum)

	// 异步执行多个任务
	step := func(stepNum int, errChan chan error) {
		defer wg.Done()
		errChan <- fmt.Errorf("step %d error", stepNum)
	}

	for i := 0; i < taskNum; i++ {
		wg.Add(1)
		go step(i, errChan)
	}

	// 等待任务完成
	go func() {
		wg.Wait()
		close(errChan)
	}()

	// err通道阻塞等待,可能的所有错误合并
	var result *multierror.Error
	for err := range errChan {
		result = multierror.Append(result, err)
	}

	// 出现一个错误时,选择记录日志或退出
	if len(result.Errors) != 0 {
		log.Println(result.Errors)
	}
}

参考文献

演示代码 https://gitee.com/wenzhou1219/go-in-prod/tree/master/error_group

errgroup源码解析 https://zhuanlan.zhihu.com/p/416054707
errgroup使用参考 https://zhuanlan.zhihu.com/p/338999914

go-multierror使用参考 https://zhuanlan.zhihu.com/p/581030231

到此这篇关于golang 错误处理channel+error真的香的文章就介绍到这了,更多相关golang 错误处理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Go语言并发编程 sync.Once

    Go语言并发编程 sync.Once

    这篇文章要介绍的是Go语言并发编程 sync.Once,sync.Once用于保证某个动作只被执行一次,可用于单例模式中,下面文章我们来介绍一下它的使用方法,需要的朋友可以参考一下
    2021-10-10
  • golang包快速生成base64验证码的方法

    golang包快速生成base64验证码的方法

    这篇文章主要介绍了golang包快速生成base64验证码的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-03-03
  • 一文带你掌握Go语言中的文件读取操作

    一文带你掌握Go语言中的文件读取操作

    这篇文章主要和大家分享一下Go语言中的文件读取操作,文中的示例代码讲解详细,对我们学习Go语言有一定的帮助,需要的小伙伴可以参考一下
    2022-12-12
  • Go高级特性之并发处理http详解

    Go高级特性之并发处理http详解

    Golang 作为一种高效的编程语言,提供了多种方法来实现并发发送 HTTP 请求,本文将深入探讨 Golang 中并发发送 HTTP 请求的最佳技术和实践,希望对大家有所帮助
    2024-02-02
  • 使用Go语言中的Context取消协程执行的操作代码

    使用Go语言中的Context取消协程执行的操作代码

    在 Go 语言中,协程(goroutine)是一种轻量级的线程,非常适合处理并发任务,然而,如何优雅地取消正在运行的协程是一个常见的问题,本文将通过一个具体的例子来展示如何使用 context 包来取消协程的执行,需要的朋友可以参考下
    2024-11-11
  • 浅谈Go语言的error类型

    浅谈Go语言的error类型

    这篇文章主要介绍了浅谈Go语言的error类型,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • Go中字符串处理 fmt.Sprintf与string.Builder的区别对比分析

    Go中字符串处理 fmt.Sprintf与string.Builder的区别对比分析

    在Go语言中,我们通常会遇到两种主要的方式来处理和操作字符串:使用fmt.Sprintf函数和string.Builder类型,本文给大家介绍它们在性能和用法上有一些关键区别,感兴趣的朋友跟随小编一起看看吧
    2023-11-11
  • Go语言实现牛顿法求平方根函数的案例

    Go语言实现牛顿法求平方根函数的案例

    这篇文章主要介绍了Go语言实现牛顿法求平方根函数的案例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • go语言的sql包原理与用法分析

    go语言的sql包原理与用法分析

    这篇文章主要介绍了go语言的sql包原理与用法,较为详细的分析了Go语言里sql包的结构、相关函数与使用方法,需要的朋友可以参考下
    2016-07-07
  • golang 调用c语言动态库方式实现

    golang 调用c语言动态库方式实现

    本文主要介绍了golang 调用c语言动态库方式实现,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-12-12

最新评论