- Published on
Golang中使用defer时注意io缓冲区刷新问题
- Authors
- Name
- ttyS3
关于defer
Golang 官方博客专门发文介绍过三条规则:
- defer语句被求值时,被defer调用的函数参数即时求值 A deferred function's arguments are evaluated when the defer statement is evaluated.
Defer statements
的Spec中有这么一句描述:
Each time a "defer" statement executes, the function value and parameters to the call are evaluated as usual and saved anew but the actual function is not invoked.
"defer" statement 的定义为:
DeferStmt = "defer" Expression .
举例:
defer f("a")
, the function value =f
, the parameters ="a"
defer f(g("a"))
, the function value =f
, the parameters =g("a")
,g("a")
会被马上求值defer f()()
,the function value =f()
, no parameters,f()
会被马上求值
关于这个第一点,应该是日常使用中需要注意比较多的,比如:
以下程序中, defer
调用的结果为输出0s
, 原因在于time.Since(startedAt)
作为fmt.Println
的参数,会被即时求值,而不是延迟到defer语句真正执行的时候。
func main() {
startedAt := time.Now()
defer fmt.Println(time.Since(startedAt)) // bug: time.Since(startedAt) 被即时求值
time.Sleep(time.Second)
}
修正方法为采用闭包: defer func() { fmt.Println(time.Since(startedAt)) }()
。
以下程序中, defer
调用的结果为输出Giney
, 而不是 Hermionie
, 虽然传递的参数是指针,但是Golang为按值传递,因此在给defer调用的函数的参数求值时, 实际上指针的地址就已经确定了。
package main
import (
"fmt"
)
type Data struct {
name string
}
func (d Data) String() string {
return fmt.Sprintf("Name: %s", d.name)
}
func main() {
ss := &Data{"Giney"}
defer func (ss *Data) {
fmt.Println(ss)
}(ss)
ss = &Data{"Hermionie"}
//ss.name = "Hermionie"
fmt.Println(ss)
}
//result:
// Name: Hermionie
// Name: Giney
- 被defer调用的函数,以后进先出的顺序执行 Deferred function calls are executed in Last In First Out order after the surrounding function returns.
- 被defer调用的函数可读取或修改即将返回到的函数的命名返回值 Deferred functions may read and assign to the returning function's named return values.
然而今天老灯要分享的这个问题跟上面三点都无关。
这个问题来自一个真实的案例。
原来的代码如下:
// buildMessage generates email message to send using net/smtp.Data()
func (e *Email) buildMessage(subject, body, to, contentType, unsubscribeLink string) (message string, err error) {
//省略无关部分代码 ...
message = addHeader(message, "Date", time.Now().Format(time.RFC1123Z))
buff := &bytes.Buffer{}
qp := quotedprintable.NewWriter(buff)
if _, err := qp.Write([]byte(body)); err != nil {
return "", err
}
defer qp.Close()
m := buff.String()
message += "\n" + m
return message, nil
}
这个方法的主要作用,除了给email添加必要的头部,其次就是调用 quotedprintable
将原始的body编码成遵循RFC 2045规范的quoted-printable编码。 新建一空的buff作为writer, 然后输入body进行编码,然后为了正常关闭quotedprintable writer, 这里用到了defer qp.Close()
。 看上去好像没啥问题。但是老灯在测试的时候,发现有时候,返回的邮件 message 的 body是空的。后面进一步调试,发现在 body 文本的内容非常短小的时候, 很大概率可重现这个bug.
那么,产生bug的原因是什么呢?就在于这个defer qp.Close()
虽然defer
能确保qp能在方法return message之前执行,但此时已经为时已晚了。因为在return之前,message
的值已经被计算过了。而此时buff.String()
取到的, 很可能是没有刷新的一个buffer, 因此在body内容非常短小的时候,可能整个值都还在缓冲区里,因此最终导致buff.String()
获得的值为空。
我们看一下quotedprintable 的 writer Close()
的实现:
// Close closes the Writer, flushing any unwritten data to the underlying
// io.Writer, but does not close the underlying io.Writer.
func (w *Writer) Close() error {
if err := w.checkLastByte(); err != nil {
return err
}
return w.flush()
}
由于Close()
隐式地调用了w.flush()
,因此,如果我们要用到write写入的东西完整内容,必须要先调用Close()
来促使writer刷新缓冲区。
修复方式当然也很简单,将defer qp.Close()
换成显式的qp.Close()
调用即可,同时注意检查错误。
参考文档:
https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-defer/
https://stackoverflow.com/questions/51360229/the-deferred-calls-arguments-are-evaluated-immediately