头图

写在前面

学习 golang ,路还很长呢,犹记得刚开始学习 golang 的时候,写起来确实非常简单,有很多包和工具使用,不需要重复造轮子,但是要真的学好一门语言作为工具,对于其原理是非常有必要学懂的

并发错误

golang 天生高并发,编码的时候,就会出现滥用 goroutine 的情况,我们来看看都是如何滥用的

func main() {
    for i := 0; i < 10; i++ {
        go func() {
            fmt.Println(" the num is ", i)
        }()
    }
    time.Sleep(time.Second)
    fmt.Println(" program over !! ")
}

xdm 看看上面这个简单的程序运行 go run main.go 输出会是什么呢?

是会输出 0 到 9 吗?,我们来实际看看效果

# go run main.go
 the num is  10
 the num is  10
 the num is  10
 the num is  10
 the num is  10
 the num is  10
 the num is  10
 the num is  10
 the num is  10
 the num is  10
 program over !!

哦豁,这是为啥,明明循环了 10 次,应该每一次递增 1 的打印出结果才对呀

其实我们看到的这种现象属于 并发错误

解决错误

我们尝试着在 匿名函数中传入参数 i, 看看效果会不会好一点

func main() {
    for i := 0; i < 10; i++ {
        go func(i int) {
            fmt.Println(" the num is ", i)
        }(i)
    }
    time.Sleep(time.Second)
    fmt.Println(" program over !! ")
}

再次执行 go run main.go 查看输出

# go run main.go
 the num is  0
 the num is  1
 the num is  2
 the num is  3
 the num is  4
 the num is  5
 the num is  6
 the num is  7
 the num is  8
 the num is  9
 program over !!

果然,这才是我们想要的结果

那么回过头来细细看代码,我们可以发现,i 是主协程中的变量,主协程会修改 i 地址上的值, 变量 i 的地址一直在被重复使用,可是多个子协程也在不停的读取 i 的值,就导致了并发错误

避免这种并发错误,就可以用我们上面用到的传参拷贝即可

探究

我们再来确认一下,是不是 i 的地址一直是同一个

func main() {
    for i := 0; i < 10; i++ {
        fmt.Printf(" i = %d , &i = %p \n", i,&i)

        go func() {
            fmt.Printf(" the i  = %d , &i = %p \n", i,&i)
        }()
    }
    time.Sleep(time.Second)
    fmt.Println(" program over !! ")
}

程序运行起来效果如下,主协程和子协程调用的 i 是同一个 i,地址完全相同

我们再来看看解决并发错误的时候,i 的地址又是有何不同

func main() {
    for i := 0; i < 10; i++ {
        fmt.Printf(" i = %d , &i = %p \n", i,&i)

        go func(i int) {
            fmt.Printf(" the i  = %d , &i = %p \n", i,&i)
        }(i)
    }
    time.Sleep(time.Second)
    fmt.Println(" program over !! ")
}

我们可以看出,主协程中的 i 地址仍然是一样的,这个没错,但是子协程里面的 i 每一个协程的 i 变量地址都不一样,每个协程输出的都是属于自己的变量 i ,因此不会有上述的错误

程序崩溃 panic

有时候我们编码,会开辟多个协程,但是没有处理好协程中可能会 panic 的情况,若子协程挂掉,那么主协程也会随之挂掉,这里我们需要特别注意

举一个简单的例子

func main() {
    for i := 0; i < 5; i++ {

        go func() {
            a := 10
            b := 0
            fmt.Printf(" the i  = %d \n", a/b)
        }()
    }
    time.Sleep(time.Second)
    fmt.Println(" program over !! ")
}

上面这个程序很明显,就是为了造 panic 的,是一个除 0 的异常,可想而知,整个程序会因为子协程的 panic 而挂掉

运行程序后报错信息如下:

# go run main.go
panic: runtime error: integer divide by zero

goroutine 5 [running]:
main.main.func1()
        /home/admin/golang_study/later_learning/goroutine_test/main.go:24 +0x11
created by main.main
        /home/admin/golang_study/later_learning/goroutine_test/main.go:21 +0x42
exit status 2

加入处理手段

我们在每一个子协程退出前都会去处理是否会有 panic,那么子协程的 panic 就不会导致 主协程挂掉了,这里谨记

func main() {
    for i := 0; i < 5; i++ {

        go func() {
            defer func() {
                if err := recover(); err != nil {
                    fmt.Println("recover one goroutine panic")
                }
            }()
            a := 10
            b := 0
            fmt.Printf(" the i  = %d \n", a/b)
        }()
    }
    time.Sleep(time.Second)
    fmt.Println(" program over !! ")
}

程序运行效果如下:

# go run main.go
recover one goroutine panic
recover one goroutine panic
recover one goroutine panic
recover one goroutine panic
recover one goroutine panic
 program over !!

很明显程序是没有 panic 的,因为每一个子协程发生的 panic 都被处理掉了,我们还可以使用 golang 提供的 runtime 包来将 具体的 panic 信息打印出来,便于分析问题

来写一个简单的例子

func main() {
    for i := 0; i < 5; i++ {

        go func() {
            defer func() {
                if err := recover(); err != nil {
                    buf := make([]byte, 512)
                    last := runtime.Stack(buf, false)
                    fmt.Println("last == ",last)
                    buf = buf[:last]
                    fmt.Printf("[PANIC]%v\n%s\n", err, buf)
                }
            }()

            a := 10
            b := 0
            fmt.Printf(" the i  = %d \n", a/b)
        }()
    }
    time.Sleep(time.Second)
    fmt.Println(" program over !! ")
}

此处我们运用了 runtime.Stack(buf, false) 来计算goroutine panic 的堆栈信息的字节数,并最终打印出来

我们先来看效果

我们将 panic 堆栈信息的字节数打印出来,并且将 panic 的具体信息也打印出来, 最重要的是程序没有崩溃

通过使用上述的方法就可以让子协程的 panic 不影响主协程的同时还可以打印出子协程 panic 的堆栈信息

可以看看源码

可以看看源码对于该函数的解释就明白了

Stack 将调用 goroutine 的堆栈跟踪格式化为 buf,并返回写入buf的字节数。

如果为 true, Stack 将格式化所有其他 goroutine 的堆栈跟踪在当前 goroutine 的跟踪之后进入 buf。

golang 的技巧还很多,咱们需要用起来才能够体现它的价值

欢迎点赞,关注,收藏

朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力

好了,本次就到这里

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

我是阿兵云原生,欢迎点赞关注收藏,下次见~


阿兵云原生
192 声望37 粉丝