#Issue# 流的关闭——踩坑之我见

#Issue# 流的关闭——踩坑之我见

本文讨论 defer + xxx.Close() 日常使用的反面教材(笑


背景&描述

最近在向第三方接口上传文件的时候遇到了点问题,大致流程:

  1. 从本地打开或从请求解析文件 I/O
  2. 创建 multipart/form-data 格式网络 I/O
  3. 将文件 I/O 流复制到网络 I/O
  4. 调用第三方的接口

最后却返回了错误提示——多媒体的数据为空,遂开始意料之外的漫漫 debug 之路…

文件 I/O 流

从本地打开

1
2
3
4
5
fileWriter, err := os.Open(filename)
if err != nil {
return err
}
defer fileWriter.Close() // better to use defer

从请求解析

1
2
3
4
5
fileWriter, _, err := ctx.Request.FormFile("key")
if err != nil {
return err
}
defer fileWriter.Close() // better to use defer

网络 I/O 流

1
2
3
4
5
6
7
8
buffer := &bytes.Buffer{} // request body
writer := multipart.NewWriter(buffer)
header := writer.FormDataContentType() // request header
connWriter, err := writer.CreateFormFile(filetype, filepath.Base(filename))
if err != nil {
return err
}
defer writer.Close() // do not use defer

将文件流复制到网络流

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
}

问题定位

排查的整体思路是由外及内,具体步骤:

  1. 排除第三方的接口错误
  2. 排除请求格式不对
  3. 排除请求数据为空
  4. 排除代码逻辑存在缺陷

最后发现是 “完美”的 defer xxx.Close() 出了问题,百思不得其解ing… _(:3」∠)_

排除第三方的因素

  • √ 使用 Postman 工具按照文档构建请求

排除请求格式问题

  • √ 重点检查请求头的 Content-TypeContent-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{} // request body
writer := multipart.NewWriter(buffer)
header := writer.FormDataContentType() // request header
connWriter, err := writer.CreateFormFile(filetype, filepath.Base(filename))
if err != nil {
return err
}

// defer writer.Close()

_, 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() 异步缓冲

  • Write() 返回 err:代表写入缓冲已经出错

  • Write() 返回 nil:代表写入缓冲已经成功

Close() 同步关闭

  • Close() 返回 err:代表从缓冲到落盘中间奇怪的bug增加了——[EIO] A previously-uncommitted write(2) encountered an input/output error.

  • Close() 返回 nil:虽然文件会被同步关闭,但是缓冲仍在异步进行

Sync() 强制落盘

  • 使用 Sync() 优点:程序能够知道最后落盘结果(成功 or 失败)

  • 使用 Sync() 缺点:异步变同步,性能会下降

实践

  • defer writer.Close() 或者 // _ = writer.Close() 都会出现空数据的错误,说明不及时地关闭文件 I/O 流影响网络 I/O 流读取

  • 如果我们延迟关闭或没有关闭输出流,由于没有一个信号告知“可以把数据从缓冲区加载至运行区了”,对端的服务器 socket session 一直被阻塞至线程超时,自然就取不到数据

解决方法

由此得出流的关闭最佳实践总结如下:

  1. 只读流可以使用 defer,读写流慎重使用 defer;文件 I/O 流可以使用 defer,网络 I/O 流慎重使用 defer
  2. 应该同等重视 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()
}

参考链接

  1. Don’t defer Close() on writable files
  2. Deferred Cleanup, Checking Errors, and Potential Problems
  3. 简单地 defer file.Close() 可能是一种错误用法
  4. socket通讯流不关闭接收不到内容

# Golang

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×