Published on

Golang中使用defer时注意io缓冲区刷新问题

Authors
  • avatar
    Name
    ttyS3
    Twitter

关于defer Golang 官方博客专门发文介绍过三条规则

  1. defer语句被求值时,被defer调用的函数参数即时求值 A deferred function's arguments are evaluated when the defer statement is evaluated.

Defer statementsSpec中有这么一句描述:

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
  1. 被defer调用的函数,以后进先出的顺序执行 Deferred function calls are executed in Last In First Out order after the surrounding function returns.
  1. 被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://medium.com/@manandharsabbir/go-lang-defer-statement-arguments-evaluated-at-defer-execution-b2c4a1687c6c

https://stackoverflow.com/questions/51360229/the-deferred-calls-arguments-are-evaluated-immediately

https://blog.golang.org/defer-panic-and-recover

https://golang.org/ref/spec#Defer_statements