本文讨论 defer
+ xxx.Close()
日常使用的反面教材(笑
背景&描述
最近在向第三方接口上传文件 的时候遇到了点问题,大致流程:
从本地打开或从请求解析文件 I/O
流
创建 multipart/form-data
格式网络 I/O
流
将文件 I/O
流复制到网络 I/O
流
调用第三方的接口
最后却返回了错误提示——多媒体的数据为空 ,遂开始意料之外的漫漫 debug
之路…
文件 I/O 流 从本地打开 1 2 3 4 5 fileWriter, err := os.Open(filename) if err != nil { return err } defer fileWriter.Close()
从请求解析 1 2 3 4 5 fileWriter, _, err := ctx.Request.FormFile("key" ) if err != nil { return err } defer fileWriter.Close()
网络 I/O 流 1 2 3 4 5 6 7 8 buffer := &bytes.Buffer{} writer := multipart.NewWriter(buffer) header := writer.FormDataContentType() connWriter, err := writer.CreateFormFile(filetype, filepath.Base(filename)) if err != nil { return err } defer writer.Close()
将文件流复制到网络流 1 2 3 4 _, err = io.Copy(connWriter, fileWriter) if err != nil { return err }
调用第三方的接口 1 2 3 4 res, err := CallThirdPartyApi(header, buffer) if err != nil { return err }
问题定位
排查的整体思路是由外及内 ,具体步骤:
排除第三方的接口错误
排除请求格式不对
排除请求数据为空
排除代码逻辑存在缺陷
最后发现是 “完美”的 defer xxx.Close()
出了问题,百思不得其解ing… _(:3」∠)_
排除第三方的因素
排除请求格式问题
√ 重点检查请求头的 Content-Type
和 Content-Disposition
1 2 3 4 5 6 7 8 9 10 ... Content-Type: multipart/form-data; boundary=${random_boundary} ... ${random_boundary} Content-Disposition: form-data; name=${name} ; filename=${filename} ; filelength=${filelength} Content-Disposition: application/octet-stream [data] ${random_boundary}
排除请求数据问题
排除代码逻辑错误
× 逐行对比别人写的类似代码,发现流的关闭写法存在差异
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 buffer := &bytes.Buffer{} writer := multipart.NewWriter(buffer) header := writer.FormDataContentType() connWriter, err := writer.CreateFormFile(filetype, filepath.Base(filename)) if err != nil { return err } _, err = io.Copy(connWriter, fileWriter) if err != nil { return err } writer.Close()
原因&验证
“But this idiom is actually harmful for writable files because deferring a function call ignores its return value, and the Close()
method can return errors. For writable files, Go programmers should avoid the defer
idiom or very infrequent, maddening bugs will occur. “
理论 Write() 异步缓冲
Close() 同步关闭
Sync() 强制落盘
实践
解决方法
由此得出流的关闭最佳实践 总结如下:
只读流可以使用 defer
,读写流慎重使用 defer
;文件 I/O
流可以使用 defer
,网络 I/O
流慎重使用 defer
应该同等重视 Write()
和 Close()
的错误,必要时可以用 Sync()
有 defer 版 单个 defer 使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func test () (err error) { defer func () { closeErr := w.Close() if err == nil { err = closeErr } }() return w.Sync() }
多个 defer 使用 1 2 3 4 5 6 7 8 9 10 11 12 13 func main () { r, err := Open("a" ) if err != nil { log.Fatalf("error opening 'a'\n" ) } defer r.Close() r, err = Open("b" ) if err != nil { log.Fatalf("error opening 'b'\n" ) } defer r.Close() }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func main () { r1, err := Open("a" ) if err != nil { log.Fatalf("error opening 'a'\n" ) } defer func () { err := r1.Close() if err != nil { log.Fatal(err) } }() r2, err := Open("b" ) if err != nil { log.Fatalf("error opening 'b'\n" ) } defer func () { err := r2.Close() if err != nil { log.Fatal(err) } }() }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func main () { r, err := Open("a" ) if err != nil { log.Fatalf("error opening 'a'\n" ) } defer Close(r) r, err = Open("b" ) if err != nil { log.Fatalf("error opening 'b'\n" ) } defer Close(r) } func Close (c io.Closer) { err := c.Close() if err != nil { log.Fatal(err) } }
无 defer 版 捕获错误 1 2 3 4 5 6 7 8 9 10 11 12 13 14 func test () error { var err error if err = w.Close(); err != nil { return err } return w.Sync() }
忽略错误 1 2 3 4 5 6 7 8 9 10 11 12 func test () error { var err error _ = w.Close() return w.Sync() }
参考链接
Don’t defer Close() on writable files
Deferred Cleanup, Checking Errors, and Potential Problems
简单地 defer file.Close() 可能是一种错误用法
socket通讯流不关闭接收不到内容