

Golang中使用defer时注意io缓冲区刷新问题
source link: https://ttys3.dev/post/golang%E4%B8%AD%E4%BD%BF%E7%94%A8defer%E6%97%B6%E6%B3%A8%E6%84%8Fio%E7%BC%93%E5%86%B2%E5%8C%BA%E5%88%B7%E6%96%B0%E9%97%AE%E9%A2%98/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Golang中使用defer时注意io缓冲区刷新问题
2020-06-22
:: 荒野無燈
June 22, 2020
关于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
Recommend
-
77
#1 — Deferred nil funcIf a deferred func evaluates to nil, execution panics when the surrounding func ends not when defer...
-
62
golang中的defer和闭包对很多初学者来说,有时候有很多坑,但是很多介绍的文章有写的乱七八糟.放假了没事可干,就稍微总结一下. 闭包 闭包有叫匿名函数,使用闭包可以使我们的代码更加优雅简洁,顾名思义匿名函数就是没有...
-
23
原文地址: Part 29: Defer 原文作者:
-
44
defer是golang提供的关键字,在函数或者方法执行完成,返回之前调用。 每次defer都会将defer函数压入栈中,调用函数或者方法结束时,从栈中取出执行,所以多个defer的执行顺序是先入后出。 defer的触发时机
-
42
README.md PHP Defer
-
14
在用Golang开发的时候, defer 这个语法也是必备的知识,但是我们除了知道他是在一个函数退出之前执行,对于 defer 是否还有其他地方需要注意的呢。 本文整理的 defer 的全场景使用情...
-
3
Golang标准库的读写文件,没有开启用户空间文件缓冲区? 版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 大家都知道写文件时数据流转的顺序是
-
8
Golang中defer、return、返回值之间执行顺序的坑 henrylee2cn · 2015-09-13 16:00:00 · 14064 次点击 · 预计阅读时间 2 分钟 · 不到1分钟之前 开始浏览 ...
-
3
golang中defer的使用规则 vikings-blog · 2017-07-01 08:00:37 · 39854 次点击 · 预计阅读时间 2 分钟 · 大约8小时之前 开始浏览 ...
-
7
Go语言中 defer 使用场景及注意事项,你是要注意的! goCenter · 11天之前 · 156 次点击 · 预计阅...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK