前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go语言中常见100问题-#79 Not closing transient resources

Go语言中常见100问题-#79 Not closing transient resources

作者头像
数据小冰
发布2022-08-15 15:29:28
2810
发布2022-08-15 15:29:28
举报
文章被收录于专栏:数据小冰
忘记关闭临时资源

在程序开发中会经常使用临时资源,这些资源必须在代码中的某个位置进行关闭以防止泄露。例如,对于操作磁盘或内存的结构体,通常可以实现io.Closer接口来表达必须关闭临时资源。本文将深入分析三个常见的示例代码,说明资源如果没有正确关闭会产生什么问题以及如何处理它们。

HTTP body

首先,我们讨论一个与HTTP相关的问题,下面程序编写了一个getBody方法,该方法会发出一个HTTP GET请求并返回HTTP正文响应。实现代码如下:

代码语言:javascript
复制
type handler struct {
        client http.Client
        url    string
}

func (h handler) getBody() (string, error) {
        resp, err := h.client.Get(h.url)
        if err != nil {
                return "", err
        }

        body, err := io.ReadAll(resp.Body)
        if err != nil {
                return "", err
        }

        return string(body), nil
}

上述程序通过http.Get请求数据并使用io.ReadAll解析响应。看起来没有问题,但是实际上存在资源泄露。resp是一个*http.Response类型,它包含一个io.ReadCloser类型字段Body。io.ReadCloser实现了io.Reader和io.Closer接口。如果http.Get请求正常返回没有出现错误,则必须要关闭resp.Body资源,否则会导致资源泄露。会造成已分配但是不再需要的内存不能被GC回收,甚至在最坏的情况下会导致客户端无法重用TCP连接。可以通过defer语句关闭resp.Body,处理起来非常方便,代码如下。

代码语言:javascript
复制
defer func() {
        err := resp.Body.Close()
        if err != nil {
                log.Printf("failed to close response: %v\n", err)
        }
}()

将body资源关闭操作放在defer语句中,保证了当getBody返回时,一定执行Close操作。注意上面的程序使用了闭包,在defer函数内部引用了外部变量resp.

「NOTE: 在服务端,实现HTTP处理程序时不需要关闭请求正文,因为服务器会自动关闭。」

此外,我们还需要知道一点,无论是否读取了body中的数据,最后body也是要必需关闭。例如,如果我们只对HTTP状态码感兴趣,而不关心正文内容,也是要对body进行关闭,否则也会导致内存泄露。

代码语言:javascript
复制
func (h handler) getStatusCode(body io.Reader) (int, error) {
        resp, err := h.client.Post(h.url, "application/json", body)
        if err != nil {
                return 0, err
        }

        defer func() {
                err := resp.Body.Close()
                if err != nil {
                        log.Printf("failed to close response: %v\n", err)
                }
        }()

        return resp.StatusCode, nil
}

像上面的程序,即使我们没有读取body中的内容,在函数返回时也是需要对其进行关闭。

还有一点需要注意的是,根据对body是否进行过数据读取,在关闭body时会产生不同的行为:

  • 如果在没有读取body的情况下对其进行关闭,默认的HTTP传输可能会关闭连接
  • 如果在读取body后对其进行关闭,默认的HTTP传输不会关闭连接。因此,它可以被重复使用。

因此,如果getStatusCode被重复调用并且我们想要利用keep-alive连接,即使我们对body内容不关心,仍然应该读取它的内容。

代码语言:javascript
复制
func (h handler) getStatusCode(body io.Reader) (int, error) {
        resp, err := h.client.Post(h.url, "application/json", body)
        if err != nil {
                return 0, err
        }

        // Close response body

        _, _ = io.Copy(io.Discard, resp.Body)

        return resp.StatusCode, nil
}

在上面的代码中,我们通过读取body中的内容以保持连接处于活动状态。需要注意的是,读取内容时没有使用io.ReadAll,而是使用io.Copy将body中的内容读到io.Discard中,io.Discard实现了io.Writer接口。此代码读取body中的内容,但直接丢弃不保存,效率比io.ReadAll更高。

「NOTE: 像下面这样,不是通过判断错误err为nil, 而是通过判断响应resp不为nil, 对body进行关闭的实现并不少见。这样的写法不是必需的,写成这样是考虑到在某些情况下(例如,重定向失败),resp和err都不是nil。然而,根据官方文档(https://github.com/golang/go/blob/master/src/net/http/client.go#L565-L567),出错时,可以忽略任何响应。仅当CheckRedirect失败时才会产生同时有非nil错误和非nil resp情况,即使这样,返回的Response.Body也已经关闭。因此,不需要 if resp!=nil{}检查。当没有错误的时候通过defer关闭body即可。」

代码语言:javascript
复制
resp, err := http.Get(url)
if resp != nil {
        defer resp.Body.Close()
}

if err != nil {
        return "", err
}

关闭资源避免泄露不仅仅是HTTP body才有,一般来说,所有实现了io.Closer接口的结构都应该在某个时候关闭。io.Closer接口包含一个Closer方法,它的定义如下:

代码语言:javascript
复制
type Closer interface {
        Close() error
}
sql.Rows

sql.Rows是执行SQL查询结果的结构类型,由于该结构实现了io.Closer接口,所以在使用完之后需要关闭。下面的程序省略了关闭操作,导致连接无法放回到连接池中,造成连接泄露。

代码语言:javascript
复制
db, err := sql.Open("postgres", dataSourceName)
if err != nil {
        return err
}

rows, err := db.Query("SELECT * FROM CUSTOMERS")
if err != nil {
        return err
}

// Use rows

return nil

我们可以在if err!=nil语句之后,调用defer函数,在函数内部通过闭包执行rows.Close操作。实现代码如下. 这样在执行Query操作后,如果没有返回错误时,可以确保rows被关闭,防止内存泄露。

代码语言:javascript
复制
// Open connection

rows, err := db.Query("SELECT * FROM CUSTOMERS")
if err != nil {
        return err
}

defer func() {
        if err := rows.Close(); err != nil {
                log.Printf("failed to close rows: %v\n", err)
        }
}()

// Use rows

「NOTE: 上面程序中的变量db类型为*sqlDB, 它代表一个连接池,该类型也实现了io.Closer接口,然而,关闭sql.DB是很少见的,因为db一般是长期存在的并且在多个goroutine之间可以共享它。」

os.File

os.File代表一个打开的文件描述符,和sql.Rows一样,在使用完成之后需要关闭。下面的程序同样使用defer来推迟调用Close方法。如果我们最后不关闭os.File.它本身不会导致泄露。因为当os.File被垃圾回收时,文件会自动关闭。但是,最好显示调用Close,因为我们不知道何时触发下一次GC(除非我们手动执行GC).

代码语言:javascript
复制
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
if err != nil {
        return err
}

defer func() {
        if err := f.Close(); err != nil {
                log.Printf("failed to close file: %v\n", err)
        }
}()

显示调用Close还有另一个好处:当我们想要主动监控返回的错误时,通过Close执行时返回的错误可以了解程序运行情况。例如,关闭可写文件描述符时,可以知道数据是否写入成功。因为向文件描述符中写入数据不是一个同步操作,出于性能考虑,数据先被写入到内存中的缓存中。BSD手册中的close(2)提到,关闭操作可能导致以前未写入的数据(仍在缓存中)遇到I/O错误时返回错误。所以,当我们向文件写入数据时,通过关闭os.File, 将关闭时可能产生的错误返回给调用方。

代码语言:javascript
复制
func writeToFile(filename string, content []byte) (err error) {
        // Open file

        defer func() {
                closeErr := f.Close()
                if err == nil {
                        err = closeErr
                }
        }()

        _, err = f.Write(content)
        return
}

上述程序对返回值进行了命名,写入操作没有返回错误时并将错误值设置为f.Close返回的结果。如果写入成功但关闭失败,即执行f.Close返回的错误非nil, 调用方可以知道writeToFile执行出现了问题,能够进行合理的处理。

此外,关闭os.File成功并不能保证文件会写入磁盘。实际中,写入仍然可能存在于文件系统上的缓冲区中,而还未刷新到磁盘上。如果内容持久化到磁盘非常重要,我们可以使用Sync()方法提交更改,在这种情况下,可以忽略Close产生的错误。下面的程序实现的一个同步写入版本,它能够保证在返回之前将内容写入磁盘,坏处是相比上面的非同步实现对性能有一定的影响。

代码语言:javascript
复制
func writeToFile(filename string, content []byte) error {
        // Open file

        defer func() {
                _ = f.Close()
        }()

        _, err = f.Write(content)
        if err != nil {
                return err
        }

        return f.Sync()
}

总结:通过上面3个案例说明关闭临时资源非常重要,否则会导致泄露。临时资源必须在恰当的时间和特定的情况下关闭。有时候,对于资源是否必须要关闭可能不是非常清楚,我们可以仔细阅读API文档或通过已有的经验来学习了解。有一点需要记住,如果一个结构体实现了io.Closer接口,最后必须要调用Close方法进行关闭。还有一点,如果闭包执行失败,我们要考虑是记录足够的日志信息还是对外抛出错误,具体怎么处理更好取决于实现。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-07-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 数据小冰 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 忘记关闭临时资源
    • HTTP body
      • sql.Rows
        • os.File
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档