這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
我已經兩次因為不恰當的省略go中的函數傳回值,一次造成MySql的too many connection錯誤,一次造成嚴重的記憶體流失。所以在這裡大家分享一下這個問題和解決辦法,也提醒自己以後不要再犯類似的錯了。
眾所周知,go中的函數可以返回多個值。但很多時候我們並不需要所有的值,而且go中定義了一個變數必須使用才可以,不然會報錯。所以對於不需要的傳回值,一般的操作方法就是省略:
for _,value := range slice{//....}
一個典型就是上面的range。range可以返回兩個值:如果後面是數組或者切片,第一值就是index索引號。如果range後面是map類型,第一值就是map的鍵key。第二值就是資料裡或者map中具體的值了。很多時候我們不需要第一個值,所以就像上面代碼中寫的一樣,直接省略就好。這樣的處理辦法在一般情況下是沒有什麼問題,但有的情況下就會出現嚴重的問題。比如下面這段代碼:
for {_, err = http.Post(url, "", nil)if err != nil {fmt.Println(err)}time.Sleep(Interval)}
因為不需要返回的資料,只要訪問不發生錯誤就行,所以我直接把第一個值省略點了。然後運行,然後就看到工作管理員裡面看到,程式進程的記憶體一直飛增。厲害的時候重新整理一次能增加2-3MB!當時就覺得有點蒙了,因為在迴圈中的就這小部分,而這部分用的都是官方的庫。當時的念頭就是難道是官方庫存在記憶體流失,想想又覺得這是不可能的。
在google上搜了半天,打算用pprof。說實話這個工具我真不會用,只是當時也沒辦法,不管合適不合適就直接上了。果然分析結果對我來說就像天書一樣。胡亂看看,只是發現goroutine增長的非常快。點進去看看,滿屏的參數也看不懂。這個時候我看到bufio這個包,突然想到以前遇到的一次錯誤。
前些時候在使用go調用MySql的時候會出現too many connection的錯誤。當時的一個原因就是我省略了一個傳回值,於是資源一直沒有釋放,最後耗盡了MySql的串連數。這次會不會一樣?於是我改了下上面的代碼:
var resp *http.Responsefor {resp, err = http.Post(url, "", nil)resp.Body.Close()if err != nil {fmt.Println(err)} time.Sleep(Interval)}
然後再運行,問題解決了!
對比兩次的代碼,我發現了問題的所在:除了因為我省略的參數裡面有需要釋放的資源,還因為兩次省略的參數都是指標!這才是關鍵!指標本質只是一個地址,並不是值的本身。所以雖然我們省略了傳回值,也只少建立了一個指標而已。而在我們調用的函數裡面,已經把變數建立好了,該消耗的記憶體已經消耗掉了。隨著不斷的迴圈,沒有釋放的資源越來越多,記憶體消耗也就越來越大了。這就是問題的關鍵。
第一次遇到這個問題的時候,簡單的以為雖然我省略了傳回值,但是go還是會建立個匿名變數什麼的,會造成記憶體的泄漏。知道這次再遇到這樣的問題,才想明白問題的本質原因是什麼。
這次的bug,給我的教訓就是,如果go裡面一個函數返回的值是指標,一定要小心,不要輕易省略。不然很有可能造成已經在函數裡面申請的記憶體空間,因為無法釋放而不斷的積累。而go文檔裡面強調要手動釋放的資源,比如http.Response.Body或者是os.File,也不要輕易的省略(一般也不會省略的……),而且一定要記住釋放,使用defer是最靠譜的(不過也有一個坑……)。