关于golang:释放有限的资源以避免泄露

7次阅读

共计 3320 个字符,预计需要花费 9 分钟才能阅读完成。

本文是对《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) ①
    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 函数返回该提早敞开语句就会被执行。

留神:咱们应该音讯 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() ①
    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
}

让咱们看一些其余对于资源须要被敞开而防止泄露的例子:

2.9.1 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。

2.9.2 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()办法来把缓冲区上的内容刷
到磁盘上。

2.9.3 压缩实现

压缩的写入和读取实现也须要被敞开的。事实上,他们创立的外部缓冲区也是须要被手动开释的。例如: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 办法。

正文完
 0