前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >Go: 如何写出内存泄露的程序

Go: 如何写出内存泄露的程序

作者头像
用户11547645
发布2025-03-07 16:04:07
发布2025-03-07 16:04:07
2200
代码可运行
举报
文章被收录于专栏:萝卜要加油萝卜要加油
运行总次数:0
代码可运行

不管使用什么语言,内存泄露是经常遇到的一类问题,然而使用Go语言编写内存泄露的代码却不容易,本文将列举几个可能出现内存泄露的场景,从反例中学习如何避免内存泄露。

资源泄露

不关闭打开的文件

当你不再需要一个打开的文件,正常你需要调用它的Close方法,如果你不调用Close,就有可能文件描述符达到最大限制,无法再打开新的文件或者连接,程序会报too many open files的错误。比如下面的例子: Code 1: 文件没关闭导致 耗尽文件描述符。

代码语言:javascript
代码运行次数:0
复制
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)  
       }  
    }  
}
----
➜  memory_leak git:(main) ✗ go run close_file.go
Error at file 61437: open test.log: too many open files

在我的Mac 电脑上一个进程能打开的文件句柄数量最大是61440,也可以手动设置这个数值。go 程序会默认打开 stderrstdoutstdin 三个文件句柄,一个进程最多能够打开61437 文件,多了就会报错。

http.Response.Body.Close()

Go 语言有一个 比较“知名”的bug,相信您一定看到过:如果我们忘记关闭 http 请求的body 的话,会导致内存泄露,比如下面的代码。 https://gist.github.com/hxzhouh/1e63ef82a1088ac378384e30651b20c9 {{< gist hxzhouh 1e63ef82a1088ac378384e30651b20c9 >}} code 2: http body 没有关闭导致内存泄露。

代码语言:javascript
代码运行次数:0
复制
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)  
    }  
}

关于这个问题您可以参考下面的文章了解更多信息,后面如果有时间的话,我们从头梳理一下 net/http

  • is resp.Body.Close() necessary if we don't read anything from the body?
  • https://manishrjain.com/must-close-golang-http-response

字符串/slice 导致内存泄露

虽然 Go spec 并没有说明一个字符串表达式的结果(子)字符串和原来字符串是否共享一个内存块 但编译器确实让它们共享一个内存块,而且很多标准库包的函数原型设计也默认了这一点。这是一个好的设计,它不仅节省内存,而且还减少了CPU消耗。 但是有时候它会造成暂时性的内存泄露。 Code 3: 字符串导致内存泄露。 https://gist.github.com/hxzhouh/e09587195e2d7aa2d5f6676777c6cb16 {{< gist hxzhouh e09587195e2d7aa2d5f6676777c6cb16>}}

![[Pasted image 20240925174837.png]] 为防止createStringWithLengthOnHeap 临时性内存泄露,我们可以使用strings.Clone() Code 4 : 使用strings.Clone() 避免临时内存泄露。

代码语言:javascript
代码运行次数:0
复制
func Demo1() {  
    for i := 0; i < 10; i++ {  
       s := createStringWithLengthOnHeap(1 << 20) //1M  
       packageStr1 = append(packageStr1, strings.Clone(s[:50]))  
    }  
}

这样就不会导致临时性内存泄露了。

goroutine leak

goroutine handler

绝大部分内存泄露的原因是因为goroutine泄露,比如下面的例子,很快将会内存耗尽 而导致OOM Code 5: goroutine handler

代码语言:javascript
代码运行次数:0
复制
for {  
       go func() {  
          time.Sleep(1 * time.Hour)  
       }()  
    }  
}

channel

channel 的使用错误也很容易导致 goroutine 泄露, 对于无缓冲的channel,必须要等到生产者和消费者全部就绪后,才能往channel写数据,否则将会阻塞。下面的例子因为Example 提前退出导致协程泄露。 Code 6: 不合理使用无缓冲channel 导致goroutine泄露

代码语言:javascript
代码运行次数:0
复制
func Example() {
    a := 1
    c := make(chanerror)
gofunc() {
        c <- err
return
    }()
// Example 在这里退出,导致协程泄露。
if a > 0 {
return
    }
    err := <-c
}

只需要改成有缓冲的channel 就能解决这个问题 c:= make(chan error,1) 还有一个典型的例子就是channel range 误用。 Code 7: 不合理使用range 导致goroutine泄露

代码语言:javascript
代码运行次数:0
复制
func main() {  
    wg := &sync.WaitGroup{}  
    c := make(chan any, 1)  
    items := []int{1, 2, 3, 4, 5}  
for _, i := range items {  
       wg.Add(1)  
gofunc() {  
          c <- i  
       }()  
    }  
gofunc() {  
for data := range c {  
          fmt.Println(data)  
          wg.Done()  
       }  
       fmt.Println("close")  
    }()  
    wg.Wait()  
    time.Sleep(1 * time.Second)  
}

channel 可以使用 range 迭代 .但是一旦读取不到内容,range 就会等待 channel 的写入,而 range 如果正好在 goroutine 内部,这个 goroutine 就会被阻塞,造成泄露。正确的做法是: 在wg.Wait() 后面 close channel.

runtime.SetFinalizer误用

如果两个对象都设置了 runtime.SetFinalizer 并且他们之间存在 "循环引⽤" ,那么这两个对象将会泄露,即时他们不再使用,GC 也不会回收他们。 关于 runtime.SetFinalizer 的更多内容,可以参考我的另外一篇文章

time.Ticker

这是go 1.23 版本之前的问题了, 如果我们不调用ticker.Stop().go 1.23 已经不会造成泄露了 https://go.dev/doc/go1.23#timer-changes

defer

我们一般习惯在defer 中释放资源 defer 函数本身不会导致内存泄露。但是它的两个机制可能会导致内存临时性泄露。

  1. 执行时间,defer 总是在函数结束的运行。如果您的函数运行时间过长,或者永远不会结束,那么您在defer 中释放的资源可能,很久都不会被释放,或者永远都不被释放。
  2. defer 本身也需要占用内存,每个 defer 都会在内存中添加一个调用点。如果您在循环中使用defer,有可能会导致临时性的内存泄露。 Code 8: defer 导致 内存临时泄露
代码语言:javascript
代码运行次数:0
复制
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()  
    }  
}

比如上面的代码,不仅仅可能会导致 defer 临时内存泄露,还可能会导致too many open files 不要痴迷于使用defer 除非你觉得代码你的代码可能会panic ,否则及时关闭文件是一个更好的选择。

总结

本文列举了几种可能会导致go 内存泄露的行为,同时 Goroutine 内存泄漏是 Go 语言最容易发生的内存泄漏情况,它通常伴随着错误地使用 goroutine 和 channel等。而 channel 的特殊用法如 select 和 range 又让 channel 阻塞变得更加隐蔽不易发现,进而增加排查内存泄漏的难度。 遇到内存泄露问题,我们可以通过 pprof 帮助我们快速的定位问题,希望我们每个人都能写出健壮的代码。

参考资料

https://go101.org/article/memory-leaking.html

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-12-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 萝卜要加油 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 资源泄露
    • 不关闭打开的文件
    • http.Response.Body.Close()
  • 字符串/slice 导致内存泄露
  • goroutine leak
    • goroutine handler
    • channel
  • runtime.SetFinalizer误用
  • time.Ticker
  • defer
  • 总结
  • 参考资料
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档