Go goroutine泄露的场景?

参考解析

泄露原因

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

泄露场景

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

nil channel

channel 如果忘记初始化,那么无论你是读,还是写操作,都会造成阻塞。

  1. func main() {
  2. fmt.Println("before goroutines: ", runtime.NumGoroutine())
  3. block1()
  4. time.Sleep(time.Second * 1)
  5. fmt.Println("after goroutines: ", runtime.NumGoroutine())
  6. }
  7. func block1() {
  8. var ch chan int
  9. for i := 0; i < 10; i++ {
  10. go func() {
  11. <-ch
  12. }()
  13. }
  14. }

输出结果:

  1. before goroutines: 1
  2. after goroutines: 11

发送不接收

channel 发送数量 超过 channel接收数量,就会造成阻塞

  1. func block2() {
  2. ch := make(chan int)
  3. for i := 0; i < 10; i++ {
  4. go func() {
  5. ch <- 1
  6. }()
  7. }
  8. }

接收不发送

channel 接收数量 超过 channel发送数量,也会造成阻塞

  1. func block3() {
  2. ch := make(chan int)
  3. for i := 0; i < 10; i++ {
  4. go func() {
  5. <-ch
  6. }()
  7. }
  8. }

http request body未关闭

resp.Body.Close() 未被调用时,goroutine不会退出

  1. e不会退出
  2. func requestWithNoClose() {
  3. _, err := http.Get("https://www.baidu.com")
  4. if err != nil {
  5. fmt.Println("error occurred while fetching page, error: %s", err.Error())
  6. }
  7. }
  8. func requestWithClose() {
  9. resp, err := http.Get("https://www.baidu.com")
  10. if err != nil {
  11. fmt.Println("error occurred while fetching page, error: %s", err.Error())
  12. return
  13. }
  14. defer resp.Body.Close()
  15. }
  16. func block4() {
  17. for i := 0; i < 10; i++ {
  18. wg.Add(1)
  19. go func() {
  20. defer wg.Done()
  21. requestWithNoClose()
  22. }()
  23. }
  24. }
  25. var wg = sync.WaitGroup{}
  26. func main() {
  27. block4()
  28. wg.Wait()
  29. }

一般发起http请求时,需要确保关闭body

defer resp.Body.Close()

互斥锁忘记解锁

第一个协程获取 sync.Mutex 加锁了,但是他可能在处理业务逻辑,又或是忘记 Unlock 了。

因此导致后面的协程想加锁,却因锁未释放被阻塞了

  1. func block5() {
  2. var mutex sync.Mutex
  3. for i := 0; i < 10; i++ {
  4. go func() {
  5. mutex.Lock()
  6. }()
  7. }

sync.WaitGroup使用不当

由于 wg.Add 的数量与 wg.Done 数量并不匹配,因此在调用 wg.Wait 方法后一直阻塞等待

  1. func block6() {
  2. var wg sync.WaitGroup
  3. for i := 0; i < 10; i++ {
  4. go func() {
  5. wg.Add(2)
  6. wg.Done()
  7. wg.Wait()
  8. }()
  9. }
  10. }

如何排查

单个函数:调用 runtime.NumGoroutine 方法来打印 执行代码前后Goroutine 的运行数量,进行前后比较,就能知道有没有泄露了。

生产/测试环境:使用PProf实时监测Goroutine的数量