本文讨论 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通讯流不关闭接收不到内容