go语言-深入defer特性[2]

385次阅读  |  发布于2年以前

别摘掉嫩芽,他们的时代才刚刚开始.....

接上文,本文介绍defer的几大特性与陷阱

延迟执行

正如在之前提到的,defer后的函数并不会立即执行,而是推迟到了函数结束后执行。这一特性一般用于资源的释放,例如在加锁之后立即延迟调用l.unlock在函数退出时即完成解锁。

l.lock()
defer l.unlock()

延迟执行的特性除了可以用于前面提到的资源释放和异常捕获,有时也用于函数的中间件。例如,对http服务器进行简单的中间件封装。该中间件的目的是捕获所有的http操作执行异常,并向客户端返回500异常。

func recoverHandler(next http.Handler) http.Handler {
    fn := func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("panic: %+v", err)
                http.Error(w, http.StatusText(500), 500)
            }
        }()

        next.ServeHTTP(w, r)
    }
    return http.HandlerFunc(fn)
}

如下所示的http服务器,可以轻松地加入recoverHandler中间件而不破坏代码原本的结构,这在第三方框架中使用广泛。

func (m MyHandler) ServeHTTP( w http.ResponseWriter,  r *http.Request) {
     fmt.Fprintf(w,"hello world")
}
func main(){
    handler :=MyHandler{}
    server:= http.Server{
        Addr:"127.0.0.1:8080",
        Handler:recoverHandler(handler),
    }
    server.ListenAndServe()
}

还可以设计一种类似计算函数执行时间的日志,也可以依靠defer函数实现。

before:= time.Now()
    defer func(){
        after := time.Now()
        fmt.Println(after.After(before))
    }()

参数预计算

defer的另一个特性是参数的预计算。这一特性时常导致开发者在使用defer时犯错。因为在大部分时候,我们记住的都是其延迟执行的特性。参数的预计算指当函数到达defer语句时,延迟调用的参数将立即求值,传递到defer函数中的参数将预先被固定,而不会等到函数执行完成后再传递参数到defer中。

如下例所示,defer后的函数需要传递int参数,首先将a赋值为1,接着defer函数的参数传递为a+1,最后,在函数返回前a被赋值为99。那么最后defer函数打印出的b值是多少呢?答案是2。原因是传递到defer的参数是预执行的,因此在执行到defer语句时,执行了a+1 并将其保留了起来,直到函数执行完成后才执行defer函数体内的语句。

func main(){
    a := 1
    defer func(b int) {
        fmt.Println("defer b",b)
    }(a+1)
    a = 99
}

defer多次执行 LIFO执行顺序

在函数体内部,可能出现多个defer函数。这些defer函数将按照后入先出(last-in,first-out LIFO)的顺序执行,这与栈的执行顺序是相同的。

func main(){
    for i:=1;i<5;i++{
        defer fmt.Println("start ",i)
    }
}

上例是一种复杂的场景,在循环中调用了defer函数。其最终的打印顺序将与正常函数的执行顺序相反,打印为:

start  4
start  3
start  2
start  1

这也意味着后申请的资源将会先得到释放,如下,加锁的顺序为m→n, 释放锁的顺序为n→m。这与习惯中资源释放的方式是相同的。

m.Lock()
    defer m.Lock()
    n.Lock()
    defer n.Lock()

defer返回值陷阱

除了前面提到的参数预计算,defer还有一种非常容易犯错的场景,涉及与返回值参数结合。如下所示,函数f中有返回值r,return g之后在defer函数中将g赋值为200。

var g = 100
func f() (r int) {
    defer func() {
        g = 200
    }()
    fmt.Printf("f: g = %d\n", g)
    return g
}
func main() {
    i := f()
    fmt.Printf("main: i = %d, g = %d\n", i, g)
}

最后程序的输出结果如下,函数执行返回值i的值为100,g的值为200,从输出结果可以推测出,在return之后,执行了defer函数。

f: g = 100
main: i = 100, g = 200

对上例中的代码稍作修改,如下所示,程序的输出结构为:main: i = 200, g = 100 。从这个结果中,我们又可以推测出defer执行完成后,执行了return语句。因为其返回值i是在defer函数中赋值的。

var g = 100
func f() (r int) {
    r = g
    defer func() {
        r = 200
    }()
    r = 0
    return r
}
func main() {
    i := f()
    fmt.Printf("main: i = %d, g = %d\n", i, g)
}

但是,上面两种代码输出结果推测出的结论是截然相反的,第1个例子推测出在return执行后,执行了defer函数,而第2个例子推测出return执行前,执行了defer函数。那么到底是怎么回事呢?原因在于return其实并不是一个原子操作,其包含了下面几步:

将返回值保存在栈上->执行defer函数->函数返回

所以第1个例子中的函数f可以翻译为如下伪代码,最终返回值r为100。

g = 100
r = g
g = 200
return

例2中函数f可以翻译为伪如下代码,最终返回值r为200。

g = 100
r = g
r = 0
r = 200
return

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8