写点什么

「Go 易错集锦」释放有限的资源以避免泄露

作者:Go学堂
  • 2022-11-30
    北京
  • 本文字数:2702 字

    阅读完需:约 9 分钟

「Go易错集锦」释放有限的资源以避免泄露

众所周知,计算机的资源(内存、磁盘)都是有限的。在编程时,这些资源必须在代码的中的某个地方被关闭释放,以避免造成资源不足而泄露。但开发人员在编写代码时往往会忽略关闭已打开的资源,从而因资源不足导致程序出现异常。


本文主要介绍在 Go 中,凡是实现了 io.Closer 接口的结构体,最终都必须要被关闭以释放资源。

一个 HTTP 中未释放资源的例子

下面这个例子是一个 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) ①    if err != nil {        return "", err    }    return string(body), nil}
复制代码


① 读取 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() ①    body, err := ioutil.ReadAll(resp.Body)    if err != nil {        return "", err    }    return string(body), nil}
复制代码


① 如果 http.Get 没有返回错误,我们会使用 defer 来关闭响应值。


在该实现中,我们使用延迟函数(defer)正确处理了关闭返回资源,这样一旦 getBody 函数返回该延迟关闭语句就会被执行。


我们应该注意的是 无论我们是否从 response.Body 中读取到内容,我们都需要把响应资源关闭


例如,在下面的函数中我们仅返回了 HTTP 状态码。然而,响应体也必须被关闭:

func getStatusCode(url string) (int, error) {    resp, err := http.Get(url)    if err != nil {        return 0, err    }    defer resp.Body.Close() ①        return resp.StatusCode, nil}
复制代码


① 即使没读取内容,响应体也需要被关闭。

何时该释放资源

我们应该确保在正确的时刻释放掉资源。例如,如果不考虑 error 的类型 就延迟调用 resp.Body.Close():

func getStatusCode(url string) (int, error) {    resp, err := http.Get(url)    defer resp.Body.Close() ①    if err != nil {        return 0, err    }
return resp.StatusCode, nil}
复制代码


① 在该阶段,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 { ①  defer resp.Body.Close() ②}if err != nil {    return "", err}
复制代码


① 如果 response 不是 nil

② 作为延迟函数关闭响应体


该实现使错误的。该实现依赖一些条件(例如,重定向失败),resp 和 err 都不是 nil。


然而,依据 Go 官方文档所说:出现错误时,任何都可以被忽略掉。一个返回非 nil 错误的非 nil 响应只有当 CheckRedirect 失败时才会出现,然而,这时返回的 Response.Body 已经被关闭了。


因此,if resp != nil {}的检查语句是没必要的。我们应该坚持最初的解决方案,只有在没有错误的情况下才在延迟函数中关闭主体。


注意: 在服务端,当实现一个 HTTP handler 时,不必关闭请求,因为它会被服务器自动关闭。


关闭资源以避免泄露不仅仅和 HTTP 的响应体有关。通常来说,所有实现了 io.Closer 接口的结构体都需要在某个时刻被关闭。该接口包含唯一的一个 Close 方法:

type Closer interface {    Close() error}
复制代码


一些需要释放资源的示例

让我们看一些其他关于资源需要被关闭而避免泄露的例子。

sql.Rows 中需要释放资源的示例

sql.Rows 是用于 sql 查询结果的结构体。因为该结构体实现了 io.Closer 接口,所以它必须被关闭。我们也可以像下面这样使用延迟函数来处理关闭逻辑:

db, err := sql.Open("postgres", dataSourceName) ①if err != nil {    return err}rows, err := db.Query("SELECT * FROM MYTABLE") ②if err != nil {    return err}defer rows.Close() ③
// Use rows
复制代码


① 创建一个 SQL 连接

② 执行一个 SQL 查询

③ 关闭 rows


如果 Query 的调用没有返回错误,那我们就需要及时的关闭 rows。

os.File 文件中需要释放资源的示例

os.File 代表一个打开的文件标识符。和 sql.Rows 一样,最终也应该的被关闭:

f, err := os.Open("events.log") ①if err != nil {    return err}defer f.Close() ②
// Use file descriptor
复制代码


① 打开文件

② 关闭文件标识符


当所在的函数块返回时我们又一次使用 defer 来调度 Close 方法。


注意:正在关闭的文件不会保证文件内容已经被写到磁盘上。事实上,写入的内容可能留在了文件系统的缓冲区上,还没有被刷新到磁盘上。如果持久化是一个关键因素,我们应该使用 Sync()方法来把缓冲区上的内容刷到磁盘上。

压缩实现中需要释放资源实例

压缩的写入和读取实现也需要被关闭的。事实上,他们创建的内部缓冲区也是需要被手动释放的。例如:gzip.Writer.

var b bytes.Buffer ①w := gzip.NewWriter(&b) ②
defer w.Close() ③
复制代码


① 创建一个缓冲区

② 创建一个新的 gzip writer

③ 关闭 gzip.Writer


gzip.Reader 具有同样的逻辑:

var b bytes.Buffer ①r, err := gzip.NewReader(&b) ②if err != nil {    return nil, err}
defer r.Close() ③
复制代码


① 创建一个缓冲区

② 创建一个新的 gzip writer

③ 关闭 gzip.Writer

小结

我们已经看到,关闭有限的资源以避免泄漏是多么重要。有限的资源必须在正确的时间和特定的场景下被关闭。有时,是否需要资源不是很明确。我们只能通过阅读相关的 API 文档或实际实践来决定。然而,我们必须要谨慎,如果一个结构体实现了 io.Closer 接口,我们就必须要在最后调用 Close 方法。


---特别推荐---

特别推荐:一个专注 go 项目实战、项目中踩坑经验及避坑指南、各种好玩的 go 工具的公众号。「Go 学堂」,专注实用性,非常值得大家关注。点击下方公众号卡片,直接关注。关注送《100 个 go 常见的错误》pdf 文档。

发布于: 刚刚阅读数: 2
用户头像

Go学堂

关注

关注「Go学堂」,学习更多编程知识 2019-08-06 加入

专注Go编程知识、案例、常见错误及原理分析。意在通过阅读更多优秀的代码,提高编程技能。同名公众号「Go学堂」期待你的关注

评论

发布
暂无评论
「Go易错集锦」释放有限的资源以避免泄露_golang_Go学堂_InfoQ写作社区