本文介绍内存泄漏的常见原因以及如何避免。
无论使用哪种编程语言,内存泄漏都是一个常见问题。本文将说明可能发生内存泄漏的几种情况,让我们学习如何通过研究这些反模式来避免内存泄漏。
未关闭已打开的文件
结束打开文件时,应始终调用其关闭方法。否则可能会导致文件描述符数量达到上限,从而无法打开新文件或连接。这可能会导致 “too many open files”错误。
代码示例 1:不关闭文件导致文件描述符耗尽。
1 | func main() { |
输出:
1 | Error at file 61437: open test.log: too many open files |
在我的 Mac 上,一个进程最多可以打开 61 440 个文件句柄。 Go 进程通常会打开三个文件描述符(stderr、stdout、stdin),因此最多只能打开 61437 个文件。可以手动调整这一限制。
未关闭 http.Response.Body
Go 有比较容易犯的错误,即忘记关闭 HTTP 请求的正文会导致内存泄漏。例如
代码示例 2:未关闭 HTTP 主体导致内存泄漏。1
2
3
4
5
6
7
8
9
10
11
12
13func makeRequest() {
client := &http.Client{}
req, err := http.NewRequest(http.MethodGet, "http://localhost:8081", nil)
res, err := client.Do(req)
if err != nil {
fmt.Println(err)
}
_, err = ioutil.ReadAll(res.Body)
// defer res.Body.Close()
if err != nil {
fmt.Println(err)
}
}
字符串和切片内存泄漏
Go 规范没有明确说明子串是否与其原始字符串共享内存。不过,编译器允许这种行为,这通常是好事,因为它减少了内存和 CPU 的使用。但有时这会导致暂时的内存泄露。
代码示例 3:字符串导致的内存泄漏。
1 | func main() { |
为了防止临时内存泄漏,我们可以使用 strings.Clone()。
代码示例 4:使用 strings.Clone() 避免临时内存泄漏。
1 | func Demo1() { |
Goroutine Handler
大多数内存泄漏都是由于程序泄漏造成的。例如,下面的示例会迅速耗尽内存,导致 OOM(内存不足)错误。
代码示例 5:goroutine 处理程序泄漏。1
2
3
4
5
6for {
go func() {
time.Sleep(1 * time.Hour)
}()
}
}
滥用 Channels
不正确地使用 channel 也很容易导致程序泄漏。对于无缓冲通道,在向通道写入数据之前,生产者和消费者都必须准备就绪,否则通道就会阻塞。在下面的示例中,函数提前退出,导致了程序泄漏。
代码示例 6:非缓冲通道滥用导致程序泄漏。1
2
3
4
5
6
7
8
9
10
11
12
13func Example() {
a := 1
c := make(chan error)
go func() {
c <- err
return
}()
// Example exits here, causing a goroutine leak.
if a > 0 {
return
}
err := <-c
}
只需将其改为缓冲通道即可解决这一问题: c := make(chan error, 1)
滥用 range with Channels
可以使用 range 遍历通道。但是,如果通道为空,range 将等待新数据,可能会阻塞 goroutine。
代码示例 7:滥用范围导致程序泄漏。
1 | func main() { |
要解决这个问题,请在调用 wg.Wait() 后关闭通道。
误用 runtime.SetFinalizer
.
如果两个对象都使用 runtime.SetFinalizer 进行了设置,并且它们相互引用,那么即使它们不再使用,也不会被垃圾回收。
time.Ticker
这是 Go 1.23 之前的一个问题。如果不调用 ticker.Stop(),可能会导致内存泄漏。Go 1.23 已修复了这个问题。
滥用 defer
虽然使用延迟释放资源不会直接导致内存泄漏,但它会通过两种方式导致临时内存泄漏:
执行时间:延迟总是在函数结束时执行。如果函数运行时间过长或从未结束,则 defer 中的资源可能无法及时释放。
内存使用:每个延迟都会在内存中增加一个调用点。如果在循环内使用,可能会导致临时内存泄漏。
代码示例 8:延迟导致的临时内存泄漏。
1 | func ReadFile(files []string) { |
这段代码会造成临时内存泄漏,并可能导致 “too many open files” 的错误。除非必要,否则应避免过度使用延迟。
结论
本文介绍了 Go 中可能导致内存泄漏的几种行为,其中最常见的是 goroutine 泄漏。通道的不当使用,尤其是选择和范围的不当使用,会增加检测泄漏的难度。当遇到内存泄漏时,pprof 可以帮助快速定位问题,确保我们编写出更健壮的代码。References。