代码改变世界

golang defer用法

2022-03-19 15:46  youxin  阅读(410)  评论(0编辑  收藏  举报

Defer

A defer statement defers the execution of a function until the surrounding function returns.

The deferred call's arguments are evaluated immediately, but the function call is not executed until the surrounding function returns.

 

package main

import "fmt"

func main() {
defer fmt.Println("world")

fmt.Println("hello")
}

 

 

https://go.dev/tour/flowcontrol/12

 

Stacking defers

Deferred function calls are pushed onto a stack. When a function returns, its deferred calls are executed in last-in-first-out order.

To learn more about defer statements read this blog post.

 

package main

import "fmt"

func main() {
    fmt.Println("counting")

    for i := 0; i < 10; i++ {
        defer fmt.Println(i)
    }

    fmt.Println("done")
}

 

官方文档:https://go.dev/ref/spec#Defer_statements

Defer statements

A "defer" statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking.

DeferStmt = "defer" Expression .

The expression must be a function or method call; it cannot be parenthesized. Calls of built-in functions are restricted as for expression statements.

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. Instead, deferred functions are invoked immediately before the surrounding function returns, in the reverse order they were deferred. That is, if the surrounding function returns through an explicit return statement, deferred functions are executed after any result parameters are set by that return statement but before the function returns to its caller. If a deferred function value evaluates to nil, execution panics when the function is invoked, not when the "defer" statement is executed.

For instance, if the deferred function is a function literal and the surrounding function has named result parameters that are in scope within the literal, the deferred function may access and modify the result parameters before they are returned. If the deferred function has any return values, they are discarded when the function completes. (See also the section on handling panics.)

lock(l)
defer unlock(l)  // unlocking happens before surrounding function returns

// prints 3 2 1 0 before surrounding function returns
for i := 0; i <= 3; i++ {
	defer fmt.Print(i)
}

// f returns 42
func f() (result int) {
	defer func() {
		// result is accessed after it was set to 6 by the return statement
		result *= 7
	}()
	return 6
}

 

在 Go 语言中的 defer 关键字就是 Go 语言中延迟语句的标志。Go 语言会在函数即将返回时按逆序执行 defer 后的语句。也就是说先被 defer 的语句后执行,最先执行最后被 defer 的语句。defer 和有些语言中的 finally 语句块的用法类似,一般都用于释放一些资源,最常用的地方就是进程锁的释放。

 

2. defer 中的变量

defer 关键字之后若有变量,则 defer 记录的是在 defer 时的变量值,而不是最后函数结束时的变量值。

代码示例:

package main

import "fmt"

func main() {
    a := 10
    defer fmt.Println("defer时a的值为", a)
    a = 100
    fmt.Println("print时a的值为", a)
}
代码块
  • 第 7 行:此时变量 a 的值为 10,所以 defer 输出时a的值为10;
  • 第 8 行:此时变量 a 的值为 100,所以输出时 a 的值为100。

 

 

 Go 语言中 defer 的使用,需要注意以下几点:

  • defer 是先声明后执行的语句模式;
  • defer 会在函数即将结束的时候统一执行;
  • defer 中的变量值不会被 defer 之后的语句改变。

 

Defer用于确保稍后在程序执行中执行函数调用,通常用于清理目的。延迟(defer)常用于例如,ensurefinally常见于其他编程语言中。

假设要创建一个文件,写入内容,然后在完成之后关闭。这里可以这样使用延迟(defer)处理。

在使用createFile获取文件对象后,立即使用closeFile推迟该文件的关闭。这将在writeFile()完成后封装函数(main)结束时执行。

运行程序确认文件在写入后关闭。

package main

import "fmt"
import "os"

// Suppose we wanted to create a file, write to it,
// and then close when we're done. Here's how we could
// do that with `defer`.
func main() {

    // Immediately after getting a file object with
    // `createFile`, we defer the closing of that file
    // with `closeFile`. This will be executed at the end
    // of the enclosing function (`main`), after
    // `writeFile` has finished.
    f := createFile("defer-test.txt")
    defer closeFile(f)
    writeFile(f)
}

func createFile(p string) *os.File {
    fmt.Println("creating")
    f, err := os.Create(p)
    if err != nil {
        panic(err)
    }
    return f
}

func writeFile(f *os.File) {
    fmt.Println("writing")
    fmt.Fprintln(f, "data")

}

func closeFile(f *os.File) {
    fmt.Println("closing")
    f.Close()
}//原文出自【易百教程】

 


Golang中多用途的defer

defer顾名思义就是延迟执行,那么defer在Golang中该如何使用以及何时使用呢?

Golang的官方时这么定义的。

1.那么在什么情况下会调用defer延迟过的函数呢?

从文档中可以知道主要有两种情况:

  1. 当函数执行了return 语句后

  2. 当函数处于panicing状态,也就是程序中panic回溯上来到当前函数范围内时

2. 如何使用defer呢?

defer可以当作一个修饰符,其不是用来修饰函数定义的,而是用来修饰函数调用的。当你调用一个函数时,在正常的书写方式的前面加上 defer 之后,该函数调用就被延迟了,具体我们来看个例子:

  1. func TestPanic(){
  2. defer func (){fmt.Println("defer function be invoked")}()
  3. fmt.Println("The first one to be invoke")
  4. return
  5. }

运行后会发现结果为:

  1. The first one to be invoke
  2. defer function be invoked

这里我们用到了一个匿名函数,但是其原型就是 fun() 形式的调用。这里符合上面的在return 语句之后执行了我们defer过的函数。

3.defer主要用于什么场景

defer最主要的使用场景有两个,一个时资源的释放,如关闭打开的文件,另一个是和panic 以及 recover 组合起来模拟try...catch 功能;。除此之外defer还可以用来对return命名返回值作修改。

资源释放当我们打开文件作操作后,很容易忘记释放文件资源。假设现在B资源要依赖A资源。如下:

  1. resA,err:= getA()
  2. if err!=nil {
  3. return
  4. }
  5. resB,err := getB(resA)
  6. if err != nile {
  7. return
  8. }

这里我们就容易产生bug,在获取B资源出错时,没有释放获取成功的A资源。这里我们添加:

  1. resA,err:= getA()
  2. defer ReleaseA()
  3. if err!=nil {
  4. return
  5. }
  6. resB,err := getB(resA)
  7. defer ReleaseB()
  8. if err != nile {
  9. return
  10. }

就可以保证,无论时在哪个位置返回,其上的资源都会得到释放。

  • 异常处理defer还有最大的用处就是和panic以及recover组合成try...catch结构。

看个示例,来自官网的:

func protect(g func()) {
    defer func() {
        log.Println("done")  // Println executes normally even if there is a panic
        if x := recover(); x != nil {
            log.Printf("run time panic: %v",x)
        }
    }()
    log.Println("start")
    g()
}

 

当g()中通过panic抛出错误时,会在defer中用recover进行捕获。也就是在子函数中的panic触发了其处于panicing状态,从而当panic回溯到当前函数时调用本函数的defer修饰的函数。

当然这里把g()替换成panic()也是可行的,就没有panic回溯了,直接时本函数中的panic触发使其处在panicing状态,从而调用defer修饰的函数。

  • 修改返回值

如同上面的例子,如果对于有异常和没有异常返回不同的值,那么该如何操作。传统的方法是:

  1. try {
  2. ...
  3. } catch (exception1){
  4. return 1;
  5. }
  6. return 2
  7. ...

由于Golang中不能在defer里面返回值,所以我们不能用上面的逻辑,在主函数和defer里面返回不同的结果。这个时候我们可以借助命名返回值来帮忙。在Golang中使用

func InvokeF()(rst int){
    defer func(){
        if err:=recover();err!=nil {
            if err== exception1 {
                rst = -1
            }
        }
    }()
    rst = f()
    return
}

 

当正常时,返回的rst为f()调用结果,当出现异常时,rst的值会被修改,从而达到目的。

4.defer中的坑

  • 栈式调用

defer的调用顺序时栈式的,也就是后修饰的函数会被先调用。如果熟悉进程和线程API的化,这里defer的调用就和at_exit()有点类似,在退出时栈式调用注册过的退出函数。在使用中我们会发现这样的方式是符合我们代码原意的。假设A资源依赖B资源。那么我们这样:

  1. resA,err := getA()
  2. defer releaseA()
  3. ...
  4. resB,err := getB(resA)
  5. defer releaseB()

一来,我们在获取资源时就注册defer,语义上跟符合人类语言。二来,当A资源获取失败时仅释放A的环境;当B资源获取失败时,释放B和A的资源,且先释放依赖A的B。这样代码更为整洁易读。

  • 出现时计算

首先要注意的是,defer修饰的函数调用中如果有参数,那么参数取的是当前计算时的值,而不是在return或者panic时该变量的值。

func main() {
    i := 2;
    deferFun := func (i int){fmt.Printf("i in panic is %d \n",i)}
    defer deferFun(i+1)
    i += 12
    fmt.Printf("i befor return is %d \n",i)
    return
}

 

得到结果为:

  1. ibeforreturnis14iinpanicis3

注意,这里只是执行了参数的计算,而并没有执行函数体,函数体时在return之后执行的。

  • 丢弃返回值

defer修饰过的函数调用的返回值时丢弃的,因此不要想着再去使用其返回值。本来也没有地方去使用。