大家好,我是木川

一、什么是 goroutine 泄露

在 Go 中,goroutine 泄露是指创建的 goroutine 没有被正确地关闭或管理,导致它们在程序运行过程中无法被回收,最终导致资源浪费和潜在的性能问题。以下是一些常见的导致 goroutine 泄露的场景

常见泄露原因如下:

  • goroutine 内进行channel/mutex 等读写操作被一直阻塞。
  • goroutine 内的业务逻辑进入死循环,资源一直无法释放。
  • goroutine 内的业务逻辑进入长时间等待,有不断新增的 goroutine 进入等待

二、泄露场景

如果输出的 goroutines 数量是在不断增加的,就说明存在泄漏

nil channel

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 发送数量 超过 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 接收数量 超过 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

sync.WaitGroup使用不当

由于 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 的数量