Go: 如何寫出內存泄露的程序
不管使用什麼語言,內存泄露是經常遇到的一類問題,然而使用 Go 語言編寫內存泄露的代碼卻不容易,本文將列舉幾個可能出現內存泄露的場景,從反例中學習如何避免內存泄露。
資源泄露
不關閉打開的文件
當你不再需要一個打開的文件, 正常你需要調用它的 Close 方法, 如果你不調用 Close,就有可能文件描述符達到最大限制,無法再打開新的文件或者連接, 程序會報too many open files
的錯誤。比如下面的例子:
Code 1: 文件沒關閉導致 耗盡文件描述符。
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 程序會默認打開 stderr
,stdout
,stdin
三個文件句柄,一個進程最多能夠打開 61437 文件,多了就會報錯。
http.Response.Body.Close()
Go 語言有一個 比較 “知名” 的 bug,相信您一定看到過:如果我們忘記關閉 http 請求的 body 的話,會導致內存泄露,比如下面的代碼。
https://gist.github.com/hxzhouh/1e63ef82a1088ac378384e30651b20c9
{{<gist hxzhouh 1e63ef82a1088ac378384e30651b20c9>}}
code 2: http body 沒有關閉導致內存泄露。
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
{{}}
![[Pasted image 20240925174837.png]]
爲防止createStringWithLengthOnHeap
臨時性內存泄露,我們可以使用strings.Clone()
Code 4 : 使用strings.Clone()
避免臨時內存泄露。
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
for {
go func() {
time.Sleep(1 * time.Hour)
}()
}
}
channel
channel 的使用錯誤也很容易導致 goroutine
泄露,
對於無緩衝的 channel,必須要等到生產者和消費者全部就緒後,才能往 channel 寫數據,否則將會阻塞。下面的例子因爲Example
提前退出導致協程泄露。
Code 6: 不合理使用無緩衝 channel 導致goroutine
泄露
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
泄露
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
函數本身不會導致內存泄露。但是它的兩個機制可能會導致內存臨時性泄露。
-
執行時間,
defer
總是在函數結束的運行。如果您的函數運行時間過長,或者永遠不會結束,那麼您在 defer 中釋放的資源可能,很久都不會被釋放,或者永遠都不被釋放。 -
defer
本身也需要佔用內存,每個
defer
都會在內存中添加一個調用點。如果您在循環中使用 defer,有可能會導致臨時性的內存泄露。
Code 8:defer
導致 內存臨時泄露
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
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/TAd18N5NkRIbp87erohteQ