Go处理并发错误

552 阅读3分钟

介绍

在go语言中,可以很方便开启go协程,但如何处理并发过程中错误是一个棘手问题。

recover goroutine中的panic

我们一般都是通过revocer来恢复程序中panic,但panic只会触发当前go协程中的defer操作。

package main

import (
	"fmt"
	"time"
)

func main() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Printf("recover panic :%v", err)
		}
	}()
	go func() {
		fmt.Println("go run")
		panic("go panic")
	}()

	time.Sleep(2 * time.Second)
	fmt.Println("exit")
}

执行结果为:

go run
panic: go panic

goroutine 6 [running]:
main.main.func2()
        D:/project/MyProject/wsn-go-sdk/并发错误/recover.go:16 +0x65
created by main.main
        D:/project/MyProject/wsn-go-sdk/并发错误/recover.go:14 +0x46
exit status 2

可以看到,程序并没有正常退出,而是由于panic异常退出。

如果想处理go协程里的panic,则修改代码如下:

package main

import (
	"fmt"
	"time"
)

func main() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Printf("recover panic :%v", err)
		}
	}()
	go func() {
		defer func() {
			if err := recover(); err != nil {
				fmt.Printf("recover go panic :%v\n", err)
			}
		}()
		fmt.Println("go run")
		panic("go panic")
	}()

	time.Sleep(2 * time.Second)
	fmt.Println("exit")
}

此时执行结果为:

go run
recover go panic :go panic
exit

go协程里的panic被捕获,程序也正常退出。

errgroup

安装

go get  golang.org/x/sync/errgroup

案例

package main

import (
	"fmt"
	"golang.org/x/sync/errgroup"
	"net/http"
)

func main() {
	var g errgroup.Group //声明一个group实例
	var urls = []string{
		"http://pkg.go.dev",
		"http://baidu.com",
		"http://www.123inafdoab.com",
	}
	for _, url := range urls {
		url := url //url是局部变量,for循环中对多个协程传递值时,需要重新赋值,否则会导致值一样。
		g.Go(func() error {
			resp, err := http.Get(url)
			if err == nil {
				resp.Body.Close()
			}
			fmt.Println(url)
			return err
		})
	}
	if err := g.Wait(); err != nil {
		fmt.Printf("go err:%v", err)
	}
}

上述对三个url地址进行请求,其中www.123inafdoab.com 是随机写的,调用时会报错,看一下执行结果。

http://www.123inafdoab.com
http://baidu.com
http://pkg.go.dev
go err:Get "http://www.123inafdoab.com": dial tcp: lookup www.123inafdoab.com: no such host

可以看到,有一个go抛错,g.wait()方法捕获到了。

源码

group实例的源码

type Group struct {
	cancel func()

	wg sync.WaitGroup

	errOnce sync.Once
	err     error
}
  • cancel 一个取消的函数,主要来包装context.WithCancel的CancelFunc
  • wg 借助于WaitGroup实现的
  • errOnce 使用sync.Once实现只输出第一个err
  • err 记录下错误的信息

然后查看g.Go方法的源码

func (g *Group) Go(f func() error) {
	g.wg.Add(1) //和waitgroup方法一样,每执行一个新的g,通过add方法加1

	go func() {
		defer g.wg.Done() //执行结束后,调用Done方法,减一

		if err := f(); err != nil { //执行传入的匿名函数
			g.errOnce.Do(func() { //如果匿名函数返回错误,会记录错误信息,这里用的once.Do保证只执行一次。
				g.err = err
				if g.cancel != nil { //如果初始化的有cancel函数,会调用cancel退出
					g.cancel()
				}
			})
		}
	}()
}

然后查看g.Wait方法的源码

func (g *Group) Wait() error {
	g.wg.Wait()//和waitgroup一样,在主线程调用wait方法,阻塞所有g执行完成。
	if g.cancel != nil {//如果初始化了cancel函数,就执行
		g.cancel()
	}
	return g.err //返回第一个出现err信息
}

结合context使用

package main

import (
	"context"
	"fmt"
	"golang.org/x/sync/errgroup"
	"time"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	group, errCtx := errgroup.WithContext(ctx)
	for index := 0; index < 3; index++ {
		indexTemp := index
		group.Go(func() error {
			fmt.Printf("第%d个协程\n", indexTemp)
			if indexTemp == 0 {
				fmt.Println("indextemp==0 start")
				fmt.Println("indextemp==0 end")
			} else if indexTemp == 1 {
				fmt.Println("indexTemp==1 start")
				//一般是协程发错异常之后,调用cancel
				cancel() //第二个协程异常退出
				fmt.Println("indexTemp==1 err")
			} else if indexTemp == 2 {
				fmt.Println("indexTemp ==2 begin")
				//休眠一秒,用于捕获协程 2的出错
				time.Sleep(1 * time.Second)
				//检查其他协程是否发成错误,如果错误,则不在执行下面代码
				err := CheckGoError(errCtx) //第三个协程感知第二个协程是否正常
				if err != nil {
					return err
				}
				fmt.Println("indexTemp==2 end")
			}
			return nil
		})
	}

	err := group.Wait()
	if err == nil {
		fmt.Println("全部完成")
	} else {
		fmt.Printf("get error:%v", err)
	}

}

func CheckGoError(errContext context.Context) error {
	select {
	case <-errContext.Done():
		return errContext.Err()
	default:
		return nil
	}
}

执行结果:

第2个协程
indexTemp ==2 begin
第1个协程
indexTemp==1 start
indexTemp==1 err
第0个协程
indextemp==0 start
indextemp==0 end
get error:context canceled