Go 学习笔记6 - defer详解 |Go主题月

160 阅读3分钟

1.介绍

// 源码 defer 结构
type _defer struct {
	siz       int32
	started   bool
	openDefer bool
	sp        uintptr
	pc        uintptr
	fn        *funcval
	_panic    *_panic
	link      *_defer
}

defer 将其后的函数调用放入了一个 list 中,查看源码中 defer 结构体的定义,可以看到通过 link 字段串联成链表。defer 传入的函数会在 defer 被包裹的函数返回之前被调用。defer 通常用于简化执行各种清理操作的功能。

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }

    written, err = io.Copy(dst, src)
    dst.Close()
    src.Close()
    return
}

上面代码有一个 bug,os.Create 如果发生错误,这时候 dst.Close() 方法不会被调用,资源得不到释放。当然我们能很方便你的把 dst.Close() 放在 err 处理里面,但是随着业务逻辑变的复杂,流程分支众多就很难维护了。通过使用 defer 完全可以解决这个问题:

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }
    defer src.Close()

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }
    defer dst.Close()

    return io.Copy(dst, src)
}

defer 语句使我们在打开每个文件后能够非常方便、正确的关闭它。确保无论函数中有多少 return 语句文件都将被关闭。

2.使用规则

defer 语句虽然好用,但是如果你掌握不好原则,那可能会出现很多让你意想不到运行结果。所以,请一定记住这几条规则!

1.当 defer 语句执行时,defer 函数中的参数将立即进行求值。
func a() {
    i := 0
    defer fmt.Println(i)
    i++
    return
}
// 输出: 0

运行结果是不是很以外? 实际上,当使用 defer 延迟调用 Println(),i 会被立即进行求值,所以 i 的值不是在函数退出前计算的,而是在 defer 调用时计算的,所以输出是 0。

再看一个例子:

func main() {
    var i int = 1

    defer fmt.Println("result =>",func() int { return i * 2 }())
    i++
    time.Sleep(3*time.Second)
}
// 输出: 2

还是按照上面的思路分析, func() int { return i * 2 }() 函数作为 Println()的参数,defer调用时它也会立即被求值,此时 i 的值是 2。

怎么解决了?使用匿名函数:

func a() {
	i := 0
	defer func() {
		fmt.Println(i)
	}()
	i++
	return
}

在 go 语言中,函数的调用都是传值的,虽然 defer 是关键字,但是也继承了这个特性。调用 defer 关键字会立刻拷贝函数中引用的外部参数。使用匿名函数后,拷贝的是函数指针,所以 函数a 退出前调用匿名函数,输出结果符合预期。

2.defer 函数调用是后进先出的 (LIFO)
func b() {
    for i := 0; i < 4; i++ {
        defer fmt.Print(i)
    }
}
// 输出 3210
3.defer 函数能够读取和分配返回函数的命名返回变量的值

原句:Deferred functions may read and assign to the returning function's named return values.

func c() (i int) {
    defer func() { i++ }()
    return 1
}
// 输出:2

要理解这条规则我们首先要理解 deferreturn返回值 三者的执行逻辑:

return 最先执行,return 负责将结果写入返回值中;接着 defer 开始执行一些收尾工作;最后函数携带当前返回值退出。

理解了这个执行逻辑顺序,那么对于第三条规则应该就能搞明白了。在上面的例子中,函数先执行 return ,然后把 1 写入命名返回变量 i 中,此时 i = 1,然最后执行 defer 后的函数。由于 defer 可以读取、修改命名返回变量的值,所以执行 i++i 的值变成了2,最后函数携带当前值返回退出。所以,你理解了吗?

func d() int {
	var i int
	defer func() { i++ }()
	return i
}
// 输出 0

自己先想想为什么这里输出值又是 0 了。

按照上面的分析,先执行 return,然后将结果 i 写入返回值中,此时 i = 0,然后执行 defer。因为 i 不是返回命名返回值,所以不能被修改,所以返回值就是0。现在是不是明白了呢?

原文参考:

defer-panic-and-recover

本篇文章在原文的基础上,添加了许多自己的理解,力求使得大家能更好的理解原文。