担心你的 Golang 程序内存泄露?看这一篇就够了!

本文介绍内存泄漏的常见原因以及如何避免。
无论使用哪种编程语言,内存泄漏都是一个常见问题。本文将说明可能发生内存泄漏的几种情况,让我们学习如何通过研究这些反模式来避免内存泄漏。

未关闭已打开的文件

结束打开文件时,应始终调用其关闭方法。否则可能会导致文件描述符数量达到上限,从而无法打开新文件或连接。这可能会导致 “too many open files”错误。

代码示例 1:不关闭文件导致文件描述符耗尽。

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {  
files := make([]*os.File, 0)
for i := 0; ; i++ {
file, err := os.OpenFile("test.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
fmt.Printf("Error at file %d: %v\n", i, err)
break
} else {
_, _ = file.Write([]byte("Hello, World!"))
files = append(files, file)
}
}
}

输出:

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
13
func 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
func main() {
Demo1()
runtime.GC()
time.Sleep(1 * time.Second)
exit()
}
func exit() {
fmt.Println("Saving heap profile...")
// create a heap profile
f, err := os.Create("heap.pprof")
if err != nil {
log.Fatal("could not create heap profile: ", err)
}
runtime.GC()
//time.Sleep(1 * time.Second)
// heap profile
if err = pprof.WriteHeapProfile(f); err != nil {
log.Fatal("could not write heap profile: ", err)
}
fmt.Println("Heap profile saved in heap.pprof")
_ = f.Close()
}

var packageStr1 []string

func Demo1() {
for i := 0; i < 10; i++ {
s := createStringWithLengthOnHeap(1 << 20) //1M
packageStr1 = append(packageStr1, s[:50])
}
}

func createStringWithLengthOnHeap(i int) string {
s := make([]byte, i)
for j := 0; j < i; j++ {
s[j] = byte(j % 256 % (rand.Intn(256) + 1))
}
return string(s)
}

为了防止临时内存泄漏,我们可以使用 strings.Clone()。

代码示例 4:使用 strings.Clone() 避免临时内存泄漏。

1
2
3
4
5
6
func Demo1() {  
for i := 0; i < 10; i++ {
s := createStringWithLengthOnHeap(1 << 20) // 1MB
packageStr1 = append(packageStr1, strings.Clone(s[:50]))
}
}

Goroutine Handler

大多数内存泄漏都是由于程序泄漏造成的。例如,下面的示例会迅速耗尽内存,导致 OOM(内存不足)错误。
代码示例 5:goroutine 处理程序泄漏。

1
2
3
4
5
6
for {  
go func() {
time.Sleep(1 * time.Hour)
}()
}
}

滥用 Channels

不正确地使用 channel 也很容易导致程序泄漏。对于无缓冲通道,在向通道写入数据之前,生产者和消费者都必须准备就绪,否则通道就会阻塞。在下面的示例中,函数提前退出,导致了程序泄漏。

代码示例 6:非缓冲通道滥用导致程序泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
func 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {  
wg := &sync.WaitGroup{}
c := make(chan any, 1)
items := []int{1, 2, 3, 4, 5}
for _, i := range items {
wg.Add(1)
go func() {
c <- i
}()
}
go func() {
for data := range c {
fmt.Println(data)
wg.Done()
}
fmt.Println("close")
}()
wg.Wait()
time.Sleep(1 * time.Second)
}

要解决这个问题,请在调用 wg.Wait() 后关闭通道。

误用 runtime.SetFinalizer.

如果两个对象都使用 runtime.SetFinalizer 进行了设置,并且它们相互引用,那么即使它们不再使用,也不会被垃圾回收。

time.Ticker

这是 Go 1.23 之前的一个问题。如果不调用 ticker.Stop(),可能会导致内存泄漏。Go 1.23 已修复了这个问题。

滥用 defer

虽然使用延迟释放资源不会直接导致内存泄漏,但它会通过两种方式导致临时内存泄漏:

  • 执行时间:延迟总是在函数结束时执行。如果函数运行时间过长或从未结束,则 defer 中的资源可能无法及时释放。

  • 内存使用:每个延迟都会在内存中增加一个调用点。如果在循环内使用,可能会导致临时内存泄漏。

代码示例 8:延迟导致的临时内存泄漏。

1
2
3
4
5
6
7
8
9
10
11
func ReadFile(files []string) {  
for _, file := range files {
f, err := os.Open(file)
if err != nil {
fmt.Println(err)
return
}
// do something
defer f.Close()
}
}

这段代码会造成临时内存泄漏,并可能导致 “too many open files” 的错误。除非必要,否则应避免过度使用延迟。

结论

本文介绍了 Go 中可能导致内存泄漏的几种行为,其中最常见的是 goroutine 泄漏。通道的不当使用,尤其是选择和范围的不当使用,会增加检测泄漏的难度。当遇到内存泄漏时,pprof 可以帮助快速定位问题,确保我们编写出更健壮的代码。References


-------------The End-------------

本文标题:担心你的 Golang 程序内存泄露?看这一篇就够了!

文章作者:cloud sjhan

发布时间:2024年10月07日 - 10:10

最后更新:2024年10月07日 - 10:10

原始链接:https://cloudsjhan.github.io/2024/10/07/担心你的-Golang-程序内存泄露?看这一篇就够了!/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

cloud sjhan wechat
subscribe to my blog by scanning my public wechat account
坚持原创技术分享,您的支持将鼓励我继续创作!
0%
;