Golang defer 快速上手
Posted 恋喵大鲤鱼
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang defer 快速上手相关的知识,希望对你有一定的参考价值。
文章目录
1.简介
defer 用于预设一个函数调用,推迟函数的执行。被推迟的函数会在执行 defer 的函数返回之前执行。
package main
import "fmt"
func main()
defer fmt.Println("world")
fmt.Println("hello")
运行输出:
hello
world
2.注意事项
2.1 defer 函数入参在 defer 时确定
被推迟函数的实参(如果该函数为方法还包括接收者)在推迟执行时就会求值,而不是在调用执行时才求值。这样不仅无需担心变量在 defer 函数执行前被改变,还意味着可以给 defer 函数传递不同实参。
for i := 0; i < 5; i++
defer fmt.Printf("%d ", i)
2.2 defer 执行顺序为后进先出
被推迟的函数按照后进先出(Last In First Out,LIFO)的顺序执行,因此以上代码在函数返回时会打印 4 3 2 1 0。
2.3 defer 函数在 return 语句赋值与返回之间执行
return 语句不是原子操作,而是被拆成了两步。
rval = xxx
ret
而 defer 函数就是在这两条语句之间执行。
rval = xxx
defer_func
ret
所以被 defer 的函数可以读取和修改带名称的返回值。
// 返回值为 2
func c() (i int)
defer func() i++ ()
return 1
2.4 defer 遇上闭包
简单来说,Go 语言中的闭包就是在函数内引用函数体之外的数据。defer 函数引用同一个外部变量时,在执行时该变量的值已经发生变化。
package main
import "fmt"
func main()
var whatever [3]struct
for i := range whatever
defer func() fmt.Println(i) ()
运行输出:
2
2
2
解决办法有两种,一种是将 i 作为实参传入,另一种是定义一个同名局部变量供 defer 函数引用。
// 将 i 作为实参传入
func main()
var whatever [3]struct
for i := range whatever
defer func(i int) fmt.Println(i) (i)
// 定义一个同名局部变量
func main()
var whatever [3]struct
for i := range whatever
i := i
defer func() fmt.Println(i) ()
运行输出:
2
1
0
2.5 defer in the loop
尽可能地不要在 for 循环中使用 defer,因为这可能会导致资源泄漏(Possible resource leak, ‘defer’ is called in the ‘for’ loop)。
defer 不是基于代码块的,而是基于函数的。你在循环中分配资源,那么不应该简单地使用 defer,因为释放资源不会尽可能早地发生(在每次迭代结束时),只有在 for 语句之后(所有迭代之后),即所在函数结束时,defer 函数才会被执行。这带来的后果就是,如果迭代次数过多,那么可能导致资源长时间得不到释放,造成泄漏。
// Bad
for rows.Next()
fields, err := db.Query(.....)
if err != nil
// ...
defer fields.Close()
// do something with `fields`
如果有一个类似上面分配资源的代码段,我们应该将其包裹在一个函数中(匿名函数或有名函数)。在该函数中,使用 defer,资源将在不需要时被立即释放。
// 1.将 defer 放在匿名函数中
for rows.Next()
func()
fields, err := db.Query(...)
if err != nil
// Handle error and return
return
defer fields.Close()
// do something with `fields`
()
// 2.将 defer 放在有名函数中然后调用之
func foo(r *db.Row) error
fields, err := db.Query(...)
if err != nil
return fmt.Errorf("db.Query error: %w", err)
defer fields.Close()
// do something with `fields`
return nil
// 调用有名函数
for rows.Next()
if err := foo(rs); err != nil
// Handle error and return
return
3.使用场景
3.1 释放资源
defer 推迟函数执行的能力显得非比寻常, 是处理一些事情的有效方式,例如无论以何种路径返回,都必须释放资源的函数。 典型的例子就是解锁和关闭文件。
//将文件的内容作为字符串返回。
func Contents(filename string) (string, error)
f, err := os.Open(filename)
if err != nil
return "", err
defer f.Close() // f.Close 会在函数结束后运行
var result []byte
buf := make([]byte, 100)
for
n, err := f.Read(buf[0:])
result = append(result, buf[0:n]...)
if err != nil
if err == io.EOF
break
return "", err // 我们在这里返回后,f 就会被关闭
return string(result), nil // 我们在这里返回后,f 就会被关闭
推迟诸如 Close 之类的函数调用有两点好处:
第一,它能确保你不会忘记关闭文件。如果你以后又为该函数添加了新的返回路径时,这种情况往往就会发生;
第二,它意味着“关闭”离“打开”很近,这总比将它放在函数结尾处要清晰明了。
3.2 跟踪函数执行
使用 defer 可以很简单地跟踪函数的执行。我们可以编写两个简单的跟踪例程,如下所示:
func trace(s string) fmt.Println("entering:", s)
func untrace(s string) fmt.Println("leaving:", s)
// Use them like this:
func a()
trace("a")
defer untrace("a")
// do something....
我们可以更好地利用这样一个事实,即延迟函数的参数在延迟执行时进行计算。可以将取消跟踪例程的参数设置为跟踪例程。
func trace(s string) string
fmt.Println("entering:", s)
return s
func un(s string)
fmt.Println("leaving:", s)
func a()
defer un(trace("a"))
fmt.Println("in a")
func b()
defer un(trace("b"))
fmt.Println("in b")
a()
func main()
b()
运行输出:
entering: b
in b
entering: a
in a
leaving: a
leaving: b
3.3 捕获 panic
对于习惯于使用其他语言进行块级资源管理的程序员来说,defer 可能看起来很奇怪,但它最有趣、最强大的应用恰恰来自这样一个事实:它不是基于块的,而是基于函数的。在捕获 panic 时便见证了这一点。
被 defer 的函数在主调函数结束前执行,这个时机点正好可以捕获主调函数抛出的 panic,因而 defer 的另一个重要用途就是执行 recover。
package main
import (
"fmt"
)
func main()
defer func()
if ok := recover(); ok != nil
fmt.Println("recover")
()
panic("error")
记住 defer 要放在 panic 执行之前。
4.小结
defer 帮助我们延迟执行函数,在使用时一定要注意相关事项,避免踩坑。另外要知晓 defer 的常用场景,灵活正确地使用,切勿滥用。
参考文献
[1] Defer - A Tour of Go
[2] Effective Go Defer
[3] The Go Programming Language Specification Defer statements
[4] 知乎.Golang之轻松化解defer的温柔陷阱
[5] stackoverflow.defer in the loop - what will be better?
以上是关于Golang defer 快速上手的主要内容,如果未能解决你的问题,请参考以下文章