大家好,我是木川
在 Go 中,goroutine 泄露是指创建的 goroutine 没有被正确地关闭或管理,导致它们在程序运行过程中无法被回收,最终导致资源浪费和潜在的性能问题。以下是一些常见的导致 goroutine 泄露的场景
常见泄露原因如下:
goroutine 内进行channel/mutex 等读写操作被一直阻塞。
goroutine 内的业务逻辑进入死循环,资源一直无法释放。
goroutine 内的业务逻辑进入长时间等待,有不断新增的 goroutine 进入等待
如果输出的 goroutines 数量是在不断增加的,就说明存在泄漏
channel 如果忘记初始化,那么无论你是读,还是写操作,都会造成阻塞。
- func block1() {
- var ch chan int // 未使用make初始化
- for i := 0; i < 10; i++ {
- go func() {
- <-ch
- }()
- }
- }
-
- func main() {
- fmt.Println("before goroutines: ", runtime.NumGoroutine())
- block1()
- time.Sleep(time.Second * 1)
- fmt.Println("after goroutines: ", runtime.NumGoroutine())
- }
输出结果:
- before goroutines: 1
- after goroutines: 11
channel 发送数量 超过 channel接收数量,就会造成阻塞
- func block2() {
- ch := make(chan int)
- for i := 0; i < 10; i++ {
- go func() {
- ch <- 1 // 没有接收者
- }()
- }
- }
-
- func main() {
- fmt.Println("before goroutines: ", runtime.NumGoroutine())
- block2()
- time.Sleep(time.Second * 1)
- fmt.Println("after goroutines: ", runtime.NumGoroutine())
- }
输出结果:
- before goroutines: 1
- after goroutines: 11
channel 接收数量 超过 channel发送数量,也会造成阻塞
- func block3() {
- ch := make(chan int)
- for i := 0; i < 10; i++ {
- go func() {
- <-ch // 没有发送者
- }()
- }
- }
-
- func main() {
- fmt.Println("before goroutines: ", runtime.NumGoroutine())
- block3()
- time.Sleep(time.Second * 1)
- fmt.Println("after goroutines: ", runtime.NumGoroutine())
- }
输出结果:
- before goroutines: 1
- after goroutines: 11
比如文件打开或http连接未正常关闭,比如 http 未调用resp.Body.Close() ,goroutine不会退出
- func requestWithNoClose() {
- _, err := http.Get("https://www.baidu.com")
- if err != nil {
- fmt.Println("error occurred while fetching page, error: %s", err.Error())
- }
- }
-
- func requestWithClose() {
- resp, err := http.Get("https://www.baidu.com")
- if err != nil {
- fmt.Println("error occurred while fetching page, error: %s", err.Error())
- return
- }
- defer resp.Body.Close()
- }
-
- func block4() {
- for i := 0; i < 10; i++ {
- wg.Add(1)
- go func() {
- defer wg.Done()
- requestWithNoClose()
- }()
- }
- }
-
- var wg = sync.WaitGroup{}
-
- func main() {
- fmt.Println("before goroutines: ", runtime.NumGoroutine())
- block4()
- wg.Wait()
- fmt.Println("after goroutines: ", runtime.NumGoroutine())
- }
输出结果:
- before goroutines: 1
- after goroutines: 21
一般发起http请求时,需要确保关闭body
defer resp.Body.Close()
第一个协程获取 sync.Mutex 加锁了,但是他可能在处理业务逻辑,又或是忘记 Unlock 了。因此导致后面的协程想加锁,却因锁未释放被阻塞了
- package main
-
- import (
- "fmt"
- "runtime"
- "sync"
- "time"
- )
-
- func block5() {
- var mutex sync.Mutex
- for i := 0; i < 10; i++ {
- go func() {
- mutex.Lock()
- }()
- }
- }
-
- func main() {
- fmt.Println("before goroutines: ", runtime.NumGoroutine())
- block5()
- time.Sleep(1 * time.Second)
- fmt.Println("after goroutines: ", runtime.NumGoroutine())
- }
输出结果:
- before goroutines: 1
- after goroutines: 11
由于 wg.Add 的数量与 wg.Done 数量并不匹配,因此在调用 wg.Wait 方法后,触发死锁检测器并导致程序崩溃
- package main
-
- import (
- "fmt"
- "sync"
- )
-
- func main() {
- var wg sync.WaitGroup
-
- wg.Add(1) // 增加计数器
- go func() {
- // 注意:没有调用 wg.Done()
- fmt.Println("Goroutine executed")
- }()
-
- // 主程序等待,但计数器没有被适当减少,触发死锁检测
- wg.Wait()
-
- fmt.Println("Main function exiting")
- }
在这个示例中,Add 增加了计数器,但在 goroutine 中没有调用 Done 减少计数器,因此计数器永远不会减少到零,这会触发死锁检测器并导致程序崩溃。
为了避免这种情况,确保在每个启动的 goroutine 中都使用 Done 减少计数器,以便计数器最终减少到零,并允许程序正常退出。这是一种良好的并发编程实践。
如果一个 goroutine 进入无限循环而没有退出的机制,它会一直运行下去,直到程序结束。
- func block7() {
- for i := 0; i < 10; i++ {
- go func() {
- for {
- // 无限循环
- }
- }()
- }
- }
-
- func main() {
- fmt.Println("before goroutines: ", runtime.NumGoroutine())
- block7()
- time.Sleep(1 * time.Second)
- fmt.Println("after goroutines: ", runtime.NumGoroutine())
- }
输出结果:
- before goroutines: 1
- after goroutines: 11
单个函数:调用 runtime.NumGoroutine 方法来打印 执行代码前后 goroutine 的运行数量,进行前后比较,就能知道有没有泄露了。
生产/测试环境:使用pprof实时监测 goroutine 的数量
最后给自己的原创 Go 面试小册打个广告,如果你从事 Go 相关开发,欢迎扫码购买,目前 10 元买断,加下面的微信发送支付截图额外赠送一份自己录制的 Go 面试题讲解视频


如果对你有帮助,帮我点一下在看或转发,欢迎关注我的公众号