Go内存泄漏分析及解决办法

文章将展示常见的Goroutine leak以及对应的解决办法。

Talk is cheap, I will show you the code.

各种泄露的展示

第一段代码来源

type Writer struct {
  queue chan []byte
}

func NewWriter() *Writer {
  w := &Writer{
    queue: make(chan []byte, 10),
  }
  go w.process()
  return w
}

func (w *Writer) Write(message []byte) {
  w.queue <- message
}

func (w *Writer) process() {
  for {
    message := <- w.queue
    // do something with message
  }
}

func main() {
  fmt.Println(runtime.NumGoroutine()) 
  test()
  fmt.Println(runtime.NumGoroutine())
}

func test() {
  NewWriter()
}

test中执行的NewWriter消失在了上下文里,Go程则存在于后台,造成了泄露。

第二段代码,做了一些增加

package main
import (
    "fmt"
    "math/rand"
    "time"
)

func queryFromSrc(src string) (ret string) {
    time.Sleep(1000)
    ret = fmt.Sprintf("query done")
    return ret
}

func multiQuery() (ret string) {
    res := make(chan string)
    go func() {
        temp := make(chan int)
        num := <- temp
        fmt.Printf("get num %d \n",num) 
    }()
    go func() {
        res <- queryFromSrc("ns2.dnsserver.com")
    }()
    return "hello"
}

func main() {
    fmt.Println("start multi query:")
    res := multiQuery()
    fmt.Println("res=", res)
}

上述的代码一共有两处导致内存泄露的部分:

第18行:从一个没有输入的管道中阻塞读取数据

第22行:向一个没有接收的管道中写入数据阻塞。

以上都会造成内存泄漏,总结可以得出: > * Go程想从一个通道读数据,但通道没有写入数据。 > * Go程想往一个通道写数据,但是这个通道没有接收方,该Go程将阻塞无法向下执行 > * 综合上述两种情况,如本文的第一段代码,Go程内形成闭环,与外部隔绝。

解决办法

1.增加管道缓存

上述第二段代码中,第22行的问题,如果,15行的代码变成res := make(chan string, 1),则不会写阻塞。最终数据会写入缓存管道,依靠Gc回收。

2.增加读超时处理

针对第18行的问题,展示一种超时处理:

go func() {
    defer done(0)
    temp := make(chan int)
    select {
        case num := <- temp:         //正常的接收
            fmt.Printf("%d \n",num)
        case <- time.After(1) :      //触发了超时
            fmt.Printf("time done stop 0 \n")
    }
}()

3.生产者close生产管道,在消费的Go程内部使用for range形式,遇到close会跳出循环。

4.全局的关闭管道, 可以做错误处理,下文的select, 当在外部因为任何原因关闭 in 时, select中in 会被触发。

package main

import (
    "fmt"
    "time"
    "runtime"
)


func test(in <- chan int, i int) {
    // 模拟的是close 关闭的情况
    for my := range in {        
        fmt.Printf(" %d is shut down \n", my)
    }
    fmt.Printf("out %d is shut down \n", i)
    
    /*
    // 模拟外部关闭的情况,in此时可以看作统一的退出管道。
    select {
        case  <- in:
            fmt.Printf(" %d is shut down \n", i)
            return
    }
    */
}

func main() {
    in := make(chan int)
    for i := 0; i < 10; i++ {
        go test(in, i)
    }
    fmt.Println(runtime.NumGoroutine())
    close(in)
    time.Sleep(10000)
    fmt.Println(runtime.NumGoroutine())
}