15-处理Go并发错误
recover goroutine中的panic
我们知道可以在代码中使用 recover 来会恢复程序中意想不到的 panic,而 panic 只会触发当前 goroutine 中的 defer 操作。
例如在下面的示例代码中,无法在 main 函数中 recover 另一个goroutine中引发的 panic。
package main
import (
"fmt"
"time"
)
func f1() {
defer func() {
if e := recover(); e != nil {
fmt.Printf("recover panic:%v\n", e)
}
}()
// 开启一个goroutine执行任务
go func() {
fmt.Println("in goroutine....")
// 只能触发当前goroutine中的defer
panic("panic in goroutine")
}()
time.Sleep(time.Second)
fmt.Println("exit")
}
func main() {
f1()
}
从输出结果可以看到程序并没有正常退出,而是由于 panic 异常退出了(exit code 2)。
➜ goStudy go run server.go
in goroutine....
panic: panic in goroutine
goroutine 6 [running]:
main.f1.func2()
/Users/yuchao/goStudy/server.go:18 +0x65
created by main.f1
/Users/yuchao/goStudy/server.go:15 +0x46
exit status 2
正如上面示例演示的那样,在启用 goroutine 去执行任务的场景下,如果想要 recover goroutine中可能出现的 panic 就需要在 goroutine 中使用 recover。就像下面的 f2 函数那样。
简单说就是,recover要写在goroutine内,配套来。
package main
import (
"fmt"
"time"
)
func f1() {
defer func() {
if e := recover(); e != nil {
fmt.Printf("recover panic:%v\n", e)
}
}()
// 开启一个goroutine执行任务
go func() {
fmt.Println("in goroutine....")
// 只能触发当前goroutine中的defer
panic("panic in goroutine")
}()
time.Sleep(time.Second)
fmt.Println("exit")
}
func f2() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("recover outer panic:%v\n", r)
}
}()
// 开启一个goroutine执行任务
go func() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("recover inner panic:%v\n", r)
}
}()
fmt.Println("in goroutine....")
// 只能触发当前goroutine中的defer
panic("panic in goroutine")
}()
time.Sleep(time.Second)
fmt.Println("exit")
}
func main() {
//f1()
f2()
}
/*
➜ goStudy go run server.go
in goroutine....
recover inner panic:panic in goroutine
exit
*/
执行 f2 函数会得到如下输出结果。
in goroutine....
recover inner panic:panic in goroutine
exit
程序中的 panic 被 recover 成功捕获,程序最终正常退出。
errorgroup
在以往演示的并发示例中,我们通常像下面的示例代码那样在 go 关键字后,调用一个函数或匿名函数。
go func(){
// ...
}
go foo()
在之前讲解并发的代码示例中我们默认被并发的那些函数都不会返回错误,但真实的情况往往是事与愿违。
当我们想要将一个任务拆分成多个子任务交给多个 goroutine 去运行,这时我们该如何获取到子任务可能返回的错误呢?
假设我们有多个网址需要并发去获取它们的内容,这时候我们会写出类似下面的代码。
package main
import (
"fmt"
"net/http"
"sync"
)
// fetchUrlDemo 并发获取url内容
func fetchUrlDemo() {
wg := sync.WaitGroup{}
var urls = []string{
"http://pkg.go.dev",
"http://www.yuchaoit.cn",
"httpp://xxx.yuchao666.xyz",
}
for _, url := range urls {
wg.Add(1)
go func(url string) {
defer wg.Done()
resp, err := http.Get(url)
if err == nil {
fmt.Printf("获取%s成功\n", url)
resp.Body.Close()
}
return // 如何将错误返回呢?
}(url)
}
wg.Wait()
// 如何获取goroutine中可能出现的错误呢?
}
func main() {
fetchUrlDemo()
}
在上面的示例代码中,我们开启了 3 个 goroutine 分别去获取3个 url 的内容。类似这种将任务分为若干个子任务的场景会有很多,那么我们如何获取子任务中可能出现的错误呢?
errgroup 包就是为了解决这类问题而开发的,它能为处理公共任务的子任务而开启的一组 goroutine 提供同步、error 传播和基于context 的取消功能。
errgroup 包中定义了一个 Group 类型,它包含了若干个不可导出的字段。
type Group struct {
cancel func()
wg sync.WaitGroup
errOnce sync.Once
err error
}
errgroup.Group 提供了Go和Wait两个方法。
func (g *Group) Go(f func() error)
- Go 函数会在新的 goroutine 中调用传入的函数f。
- 第一个返回非零错误的调用将取消该Group;下面的Wait方法会返回该错误
func (g *Group) Wait() error
- Wait 会阻塞直至由上述 Go 方法调用的所有函数都返回,然后从它们返回第一个非nil的错误(如果有)。
解决for range刻舟求剑问题
for循环读取出来的值都是放在一个内存地址中的(好比是同一个瓢)
但是我们想要的是每次瓢盛出来的水不是吗?
package main
import (
"fmt"
"sync"
)
var urls = []string{
"http://baidu.com",
"http://www.yuchaoit.cn",
"http://www.yuchao666.xyz",
}
/* 处理刻舟求剑,方案1
func main() {
var wg sync.WaitGroup
for _, url := range urls {
newUrl := url //方案1:解决了刻舟求剑的问题,用局部变量,保存每次循环的值
wg.Add(1)
//如果不处理,协程拿的是range 创建的url这一个变量,每次拿的都是最后一个url
go func() {
fmt.Printf("%p、%v\n", &newUrl, newUrl)
wg.Done()
}()
}
wg.Wait()
}
*/
//处理刻舟求剑,方案2
func main() {
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go func(newUrl string) {
fmt.Printf("%p、%v\n", &newUrl, newUrl)
wg.Done()
}(url) //方案2,传递参数,实现值拷贝,确保每次协程拿到单独的值
}
wg.Wait()
}