一说到 go channel,很多人会使用“优秀”“哲学”这些词汇来描述。殊不知,go channel 恐怕还是 golang 中最容易造成问题的特性之一。很多情况下,我们使用 go channel 时,常常以为可以关闭 channel,但实际上却没有关闭,这就是导致 go channel 内存泄漏的元凶。
阅读本文前要求读者熟悉 go channel 的基本知识。如果你不够了解 go channel,那么可以先阅读《新手使用 go channel 需要注意的问题》。本文会默认你已经了解相关内容。
情境一:select-case
误用导致的内存泄露
废话说少,先看代码。
func TestLeakOfMemory(t *testing.T) {
fmt.Println("NumGoroutine:", runtime.NumGoroutine())
chanLeakOfMemory()
time.Sleep(time.Second * 3) // 等待 goroutine 执行,防止过早输出结果
fmt.Println("NumGoroutine:", runtime.NumGoroutine())
}
func chanLeakOfMemory() {
errCh := make(chan error) // (1)
go func() {
// (5)
time.Sleep(2 * time.Second)
errCh <- errors.New("chan error") // (2)
fmt.Println("finish sending")
}()
var err error
select {
case <-time.After(time.Second): // (3) 大家也经常在这里使用 <-ctx.Done()
fmt.Println("超时")
case err = <-errCh: // (4)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(nil)
}
}
}
执行代码(需注意,使用测试和 main 线程执行的输出略有不同)
大家认为输出的结果是什么?正确的输出结果如下:
NumGoroutine: 2
超时
NumGoroutine: 3
这是 go channel 导致内存泄漏的经典场景。根据输出结果(开始有两个 goroutine,结束时有三个 goroutine),我们可以知道,直到测试函数结束前,仍有一个 goroutine 没有退出。原因是由于 (1) 处创建的 errCh 是不含缓存队列的 channel,如果 channel 只有发送方发送,那么发送方会阻塞;如果 channel 只有接收方