likes
comments
collection
share

Go精进之路 | defer

作者站长头像
站长
· 阅读数 33

引言

在开发中经常会编写在函数中申请一些资源并在函数退出前释放的代码,这类代码需要特殊关注错误处理,确保这些这些资源可以正确地被释放。Go 语言提供了 defer 语句用于确保在函数返回前执行清理工作,从而提高了程序的可读性和健壮性,降低了开发人员的心智负担。本文介绍了 defer 的执行过程、实现机制和常见用法。

执行过程和实现机制

defer 的执行基于函数,只有函数和方法的内部才能使用 defer

defer 关键字后面只能接函数或方法,这些函数被称为 deferred 函数。deferred 函数的参数在 defer 语句执行时就立即确定,如果 defer 语句中的函数使用了任何变量,那么它们的值是在 defer 语句被执行时的值,而非函数结束时的值。

defer 语句将 deferred 函数注册到其所在 goroutine 用于存放 deferred 函数的栈中,按后进先出的顺序调度执行。

Go精进之路 | defer

常见用法

拦截 panic

defer 运行机制决定了无论是正常返回或错误分支显式返回和发生 panic,已经注册的 defered 函数都会被调度执行。因此 defer 可以拦截 panic,并对 panic 按需处理。

func bar() {
    fmt.Println("raise a panic")
    panic(-1)
}

func foo() {
    defer func() {
        if e := recover(); e != nil {
            fmt.Println("recovered from a panic")
        }
    }()
    bar()
}

func main() {
    foo()
    fmt.Println("main exit normally")
}

在这段代码中就使用了 defered 函数成功拦截并恢复 panic。

修改函数具名返回值

具名返回值是在函数定义时指定的返回变量名。这些变量在函数开始执行时就被初始化,并在整个函数体内可用。 当使用 defered 函数时,可以在这个 defered 函数中修改具名返回值。即使在执行 return 时已经确定了返回值,defered 函数中的修改依然会影响最终的返回结果。

func foo(a, b int)(x, y int) {
    defer func() { 
        x = x * 5 
        y = y * 10 
    }() 
    
    x = a + 5 
    y = b + 6 
    return 
} 
func main() { 
    x, y := foo(1, 2) 
    fmt.Println("x=", x, "y=", y) 
}

这段程序的最终输出结果是 x=30 y=80

输出调试信息

deferred 函数被注册及调度执行的时间点使得它十分适合用来输出一些调试信息。如在出入函数时打印日志

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

常见问题

什么情况下会不执行 defer

  1. 程序崩溃(Panic)未恢复(Recover)

当程序发生panic时,Go的运行时会查找是否有对应的recover调用来捕获和处理这个panic。如果一个panic被recover捕获,那么程序会继续运行,并且所有注册的defer语句都会执行。然而,如果这个panic没有被任何recover捕获,那么程序将会异常终止,所有在panic点之后的代码,包括defer语句,都不会执行。

  1. 调用os.Exit()

os.Exit()导致程序立即退出,它不像panic那样触发栈的展开过程(unwinding),因此不会触发defer语句的执行。这是由于os.Exit()直接向操作系统发送终止信号,绕过了Go语言的正常运行时终止流程。

转载自:https://juejin.cn/post/7377578894591967242
评论
请登录