本文是对 《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办法。