在使用Golang 调用net/http客户端的时候,如果概念不清楚,经常会发生文件描述符泄露的情况。
垃圾回收和资源回收
首先要搞清楚的概念是
- 垃圾回收
- 资源回收
垃圾回收我们指的是在变量不使用的情况下,Go语言的垃圾回收机制可以帮助我们自动回收内存。与之对应的概念应该是手动回收垃圾,比如我们c语言中malloc了一块内存,需要手动free释放。
资源回收我们指不使用的资源需要关闭、回收、释放。这个Go并没有自动的机制。比如我们打开的一个文件,占用了一个文件描述符;如果不关闭文件,则一直占用这个文件描述符,导致泄露。
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
ticker := time.NewTicker(1 * time.Second)
client := &http.Client{}
for {
select {
case <-ticker.C:
req, err := http.NewRequest("GET", "https://www.baidu.com", nil)
if err != nil {
panic(err)
}
res, err := client.Do(req)
if err != nil {
panic(err)
}
fmt.Println(res.Status)
// res.Body.Close() // 正确方法应该把这一行注释去掉
// defer res.Body.Close() // 完全错误的写法, 请看下面 **defer 延迟调用** 的解释
}
}
}
当我们调用 res.Body.Close() 随着时间增加,打开的文件逐渐增多
defer 延迟调用
在代码过程中,我还犯了另一个错误,忽略了defer的调用时机。我原以为,defe是就好比c++智能指针那种技术,是把函数注册到变量的析构函数中,然后在变量失去作用域之后,被自动调用。
其实 defer 向函数注册退出调用,即主函数退出时,defer后的函数才被调用。
而上面的代码,由于使用了死循环,函数没有机会退出,所以导致 关闭文件的函数永远不会被调用。
我们可以重新定义一个函数,来解决这个问题。
func call_req(client *http.Client, request *http.Request) {
res, err := client.Do(request)
defer res.Body.Close()
if err != nil {
panic(err)
}
fmt.Println(res.Status)
}
func main() {
ticker := time.NewTicker(1 * time.Second)
client := &http.Client{}
for {
select {
case <-ticker.C:
req, err := http.NewRequest("GET", "https://www.baidu.com", nil)
if err != nil {
panic(err)
}
call_req(client, req)
}
}
}