本文是对 《100 Go Mistackes:How to Avoid Them》 一书的翻译. 因翻译水平有限, 难免存在翻译准确性问题, 敬请谅解. 关注 公众号 "Go 学堂", 获取更多系列文章
众所周知, 计算机的资源 (内存, 磁盘) 都是有限的, 在编程时, 这些资源必须在代码的中的某个地方被关闭释放, 以避免造成资源不足而泄露. 但开发人员在编写代码时往往会忽略关闭已打开的资源, 从而因资源不足导致程序出现异常.
本文主要介绍在 Go 中, 凡是实现了 io.Closer 接口的结构体, 最终都必须要被关闭以释放资源.
下面这个例子是一个 getBody 函数, 该函数会构建一个 HTTP GET 请求并处理得到的 HTTP 响应.
下面是第一版本的实现:
- func getBody(url string) (string, error) {
- resp, err := http.Get(url)
- if err != nil {
- return "", err
- }
- body, err := ioutil.ReadAll(resp.Body) 1
- if err != nil {
- return "", err
- }
- return string(body), nil
- }
1 读取 resp.Body 并将其转换成一个字节数组[]byte
我们使用了 http.Get 方法, 然后我们使用 ioutil.ReadAll 解析响应值. 这个函数的功能看起来算是正常的. 至少, 它正确返回了 HTTP 响应.
然而, 这里存在一个资源泄露的问题. 让我们看看是在哪里.
resp 是一个 * http.Response 指针类型. 它包含一个 io.ReaderCloser 字段(io.ReadCloser 同时包含 io.Reader 接口和 io.Closer 接口). 如果 http.Get 没有返回错误, 那该字段必须被关闭. 否则, 就会造成资源泄露. 它会占用一些内存, 这些内存在函数执行后就不再需要了, 但因没有主动释放资源所以不能被 GC 回收, 同时在资源匮乏的时候客户端还不能重用 TCP 连接.
处理该主体关闭的最方便的方法就是使用 defer 语句:
- func getBody(url string) (string, error) {
- resp, err := http.Get(url)
- if err != nil {
- return "", err
- }
- defer resp.Body.Close() 1
- body, err := ioutil.ReadAll(resp.Body)
- if err != nil {
- return "", err
- }
- return string(body), nil
- }
1 如果 http.Get 没有返回错误, 我们会使用 defer 来关闭响应值.
在该实现中, 我们使用延迟函数 (defer) 正确处理了关闭返回资源, 这样一旦 getBody 函数返回该延迟关闭语句就会被执行.
注意: 我们应该消息 resp.Body.Close()返回错误. 我们在错误管理一章将会看到在延迟函数中如何处理错误. 在这个例子以及后续的例子中, 我们将暂时忽略错误.
我们应该注意的是 无论我们是否从 response.Body 中读取到内容, 我们都需要把响应资源关闭.
例如, 在下面的函数中我们仅返回了 HTTP 状态码. 然而, 响应体也必须被关闭:
- func getStatusCode(url string) (int, error) {
- resp, err := http.Get(url)
- if err != nil {
- return 0, err
- }
- defer resp.Body.Close() 1
- return resp.StatusCode, nil
- }
1 即使没读取内容, 响应体也需要被关闭.
我们应该确保在正确的时刻释放掉资源. 例如, 如果不考虑 error 的类型 就延迟调用 resp.Body.Close():
- func getStatusCode(url string) (int, error) {
- resp, err := http.Get(url)
- defer resp.Body.Close() 1
- if err != nil {
- return 0, err
- }
- return resp.StatusCode, nil
- }
1 在该阶段, resp 可能是 nil
因为 resp 可能是 nil, 所以这段代码可能会导致程序 panic:
panic: runtime error: invalid memory address or nil pointer dereference
最后一件关于 HTTP 请求体关闭需要注意的事情. 一个非常少见的情况, 就是如果响应是空, 而非 nil 时关闭响应:
- resp, err := http.Get(url)
- if resp != nil { 1
- defer resp.Body.Close() 2
- }
- if err != nil {
- return "", err
- }
1 如果 response 不是 nil
2 作为延迟函数关闭响应体
该实现使错误的. 该实现依赖一些条件(例如, 重定向失败),resp 和 err 都不是 nil.
然而, 依据 Go 官方文档所说: 出现错误时, 任何都可以被忽略掉. 一个返回非 nil 错误的非 nil 响应只有当 CheckRedirect 失败时才会出现, 然而, 这时返回的 Response.Body 已经被关闭了.
因此, if resp != nil {}的检查语句是没必要的. 我们应该坚持最初的解决方案, 只有在没有错误的情况下才在延迟函数中关闭主体.
注意: 在服务端, 当实现一个 HTTP handler 时, 不必关闭请求, 因为它会被服务器自动关闭.
关闭资源以避免泄露不仅仅和 HTTP 的响应体有关. 通常来说, 所有实现了 io.Closer 接口的结构体都需要在某个时刻被关闭. 该接口包含唯一的一个 Close 方法:
- type Closer interface {
- Close() error
- }
让我们看一些其他关于资源需要被关闭而避免泄露的例子:
2.9.1 sql.Rows
sql.Rows 是用于 sql 查询结果的结构体. 因为该结构体实现了 io.Closer 接口, 所以它必须被关闭. 我们也可以像下面这样使用延迟函数来处理关闭逻辑:
- db, err := sql.Open("postgres", dataSourceName) 1
- if err != nil {
- return err
- }
- rows, err := db.Query("SELECT * FROM MYTABLE") 2
- if err != nil {
- return err
- }
- defer rows.Close() 3
- // Use rows
1 创建一个 SQL 连接
2 执行一个 SQL 查询
3 关闭 rows
如果 Query 的调用没有返回错误, 那我们就需要及时的关闭 rows.
2.9.2 os.File
os.File 代表一个打开的文件标识符. 和 sql.Rows 一样, 最终也应该的被关闭:
- f, err := os.Open("events.log") 1
- if err != nil {
- return err
- }
- defer f.Close() 2
- // Use file descriptor
1 打开文件
2 关闭文件标识符
当所在的函数块返回时我们又一次使用 defer 来调度 Close 方法.
注意: 正在关闭的文件不会保证文件内容已经被写到磁盘上. 事实上, 写
入的内容可能留在了文件系统的缓冲区上, 还没有被刷新到磁盘上. 如果
持久化是一个关键因素, 我们应该使用 Sync()方法来把缓冲区上的内容刷
到磁盘上.
2.9.3 压缩实现
压缩的写入和读取实现也需要被关闭的. 事实上, 他们创建的内部缓冲区也是需要被手动释放的. 例如: gzip.Writer.
- var b bytes.Buffer 1
- w := gzip.NewWriter(&b) 2
- defer w.Close() 3
1 创建一个缓冲区
2 创建一个新的 gzip writer
3 关闭 gzip.Writer
gzip.Reader 具有同样的逻辑:
- var b bytes.Buffer 1
- r, err := gzip.NewReader(&b) 2
- if err != nil {
- return nil, err
- }
- defer r.Close() 3
1 创建一个缓冲区
2 创建一个新的 gzip writer
3 关闭 gzip.Writer
小结
我们已经看到, 关闭有限的资源以避免泄漏是多么重要. 有限的资源必须在正确的时间和特定的场景下被关闭. 有时, 是否需要资源不是很明确. 我们只能通过阅读相关的 API 文档或实际实践来决定. 然而, 我们必须要谨慎, 如果一个结构体实现了 io.Closer 接口, 我们就必须要在最后调用 Close 方法.
来源: https://segmentfault.com/a/1190000040568814