介绍
在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