我已经两次因为不恰当的省略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.Response for { 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是最靠谱的(不过也有一个坑……)。