写在前面
学习 golang ,路还很长呢,犹记得刚开始学习 golang 的时候,写起来确实非常简单,有很多包和工具使用,不需要重复造轮子,但是要真的学好一门语言作为工具,对于其原理是非常有必要学懂的。
并发错误
golang 天生高并发,编码的时候,就会出现滥用 goroutine 的情况,我们来看看都是如何滥用的
funcmain(){fori:=0;i<10;i++{gofunc(){fmt.Println("thenumis",i)}()}time.Sleep(time.Second)fmt.Println("programover!!")}
xdm 看看上面这个简单的程序运行 go run main.go 输出会是什么呢?
是会输出 0 到 9 吗?,我们来实际看看效果
#gorunmain.gothenumis10thenumis10thenumis10thenumis10thenumis10thenumis10thenumis10thenumis10thenumis10thenumis10programover!!
哦豁,这是为啥,明明循环了 10 次,应该每一次递增 1 的打印出结果才对呀
其实我们看到的这种现象属于 并发错误
解决错误
我们尝试着在 匿名函数中传入参数 i, 看看效果会不会好一点
funcmain(){fori:=0;i<10;i++{gofunc(iint){fmt.Println("thenumis",i)}(i)}time.Sleep(time.Second)fmt.Println("programover!!")}
再次执行 go run main.go
查看输出
#gorunmain.gothenumis0thenumis1thenumis2thenumis3thenumis4thenumis5thenumis6thenumis7thenumis8thenumis9programover!!
果然,这才是我们想要的结果
那么回过头来细细看代码,我们可以发现,i 是主协程中的变量,主协程会修改 i 地址上的值, 变量 i 的地址一直在被重复使用,可是多个子协程也在不停的读取 i 的值,就导致了并发错误
避免这种并发错误,就可以用我们上面用到的传参拷贝即可
探究
我们再来确认一下,是不是 i 的地址一直是同一个
funcmain(){fori:=0;i<10;i++{fmt.Printf("i=%d,&i=%p\n",i,&i)gofunc(){fmt.Printf("thei=%d,&i=%p\n",i,&i)}()}time.Sleep(time.Second)fmt.Println("programover!!")}
程序运行起来效果如下,主协程和子协程调用的 i 是同一个 i,地址完全相同
我们再来看看解决并发错误的时候,i 的地址又是有何不同
funcmain(){fori:=0;i<10;i++{fmt.Printf("i=%d,&i=%p\n",i,&i)gofunc(iint){fmt.Printf("thei=%d,&i=%p\n",i,&i)}(i)}time.Sleep(time.Second)fmt.Println("programover!!")}
我们可以看出,主协程中的 i 地址仍然是一样的,这个没错,但是子协程里面的 i 每一个协程的 i 变量地址都不一样,每个协程输出的都是属于自己的变量 i ,因此不会有上述的错误
程序崩溃 panic
有时候我们编码,会开辟多个协程,但是没有处理好协程中可能会 panic 的情况,若子协程挂掉,那么主协程也会随之挂掉,这里我们需要特别注意
举一个简单的例子
funcmain(){fori:=0;i<5;i++{gofunc(){a:=10b:=0fmt.Printf("thei=%d\n",a/b)}()}time.Sleep(time.Second)fmt.Println("programover!!")}
上面这个程序很明显,就是为了造 panic 的,是一个除 0 的异常,可想而知,整个程序会因为子协程的 panic 而挂掉
运行程序后报错信息如下:
#gorunmain.gopanic:runtimeerror:integerdividebyzerogoroutine5[running]:main.main.func1()/home/admin/golang_study/later_learning/goroutine_test/main.go:24+0x11createdbymain.main/home/admin/golang_study/later_learning/goroutine_test/main.go:21+0x42exitstatus2
加入处理手段
我们在每一个子协程退出前都会去处理是否会有 panic,那么子协程的 panic 就不会导致 主协程挂掉了,这里谨记
funcmain(){fori:=0;i<5;i++{gofunc(){deferfunc(){iferr:=recover();err!=nil{fmt.Println("recoveronegoroutinepanic")}}()a:=10b:=0fmt.Printf("thei=%d\n",a/b)}()}time.Sleep(time.Second)fmt.Println("programover!!")}
程序运行效果如下:
#gorunmain.gorecoveronegoroutinepanicrecoveronegoroutinepanicrecoveronegoroutinepanicrecoveronegoroutinepanicrecoveronegoroutinepanicprogramover!!
很明显程序是没有 panic 的,因为每一个子协程发生的 panic 都被处理掉了,我们还可以使用 golang 提供的 runtime 包来将 具体的 panic 信息打印出来,便于分析问题
来写一个简单的例子
#gorunmain.gothenumis10thenumis10thenumis10thenumis10thenumis10thenumis10thenumis10thenumis10thenumis10thenumis10programover!!0
此处我们运用了 runtime.Stack(buf, false)
来计算goroutine panic 的堆栈信息的字节数,并最终打印出来
我们先来看效果
我们将 panic 堆栈信息的字节数打印出来,并且将 panic 的具体信息也打印出来, 最重要的是程序没有崩溃
通过使用上述的方法就可以让子协程的 panic 不影响主协程的同时还可以打印出子协程 panic 的堆栈信息
可以看看源码
可以看看源码对于该函数的解释就明白了
Stack 将调用 goroutine 的堆栈跟踪格式化为 buf,并返回写入buf的字节数。
如果为 true, Stack 将格式化所有其他 goroutine 的堆栈跟踪在当前 goroutine 的跟踪之后进入 buf。
golang 的技巧还很多,咱们需要用起来才能够体现它的价值。