Go defer, panic&recover

陆铭恒

Go defer, panic&recover

定义与使用

defer

A defer statement pushes a function call onto a list. The list of saved calls is executed after the surrounding function returns. Defer is commonly used to simplify functions that perform various clean-up actions.

defer语句将函数调用放进一个栈,并在最终函数return前调用这些函数。也就是将一系列函数延迟执行,常常用来做一些清理操作。

那为什么需要defer?直接在函数return前调用这些函数不行?看个例子

example

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
}

假如open和create文件操作都正常的话,上面这个函数是没问题的。但是如果,正常open了文件,而create文件失败的话,CopyFile函数返回,这时,open打开的文件流就没有被关闭。当然,也可以在每个return之前尝试cloese打开的文件,但这样很啰嗦麻烦,而且上面还是比较简单的demo。

所以defer也是因为Go的错误处理机制衍生出来的?

上面的example可以使用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)
}

注意

  1. defer函数的传参在声明的时候便已经赋值
func a() {
    i := 0
    defer fmt.Println("i = "i)
    i++
    return
}

// output
// i = 0
  1. defer声明的函数是放进一个栈,所以按后进先出的顺序执行
func a() {
  defer fmt.Println("defer 1")
  defer fmt.Println("defer 2")
}

// output
// defer 1
// defer 2
  1. defer可能会影响命名返回值
func c() (i int) {
    defer func() { i++ }()
    return 1
}

c函数最终返回 2而不是1。因为defer 在retun之前执行, 假设是 return a+b , 执行顺序是先a+b, 再defer, 最后return 。

上面的例子等同于

i = 1
func() {i++}()
return i

panic & recover

panic

The panic built-in function stops normal execution of the current goroutine. When a function F calls panic, normal execution of F stops immediately. Any functions whose execution was deferred by F are run in the usual way, and then F returns to its caller. To the caller G, the invocation of F then behaves like a call to panic, terminating G's execution and running any deferred functions. This continues until all functions in the executing goroutine have stopped, in reverse order. At that point, the program is terminated with a non-zero exit code. This termination sequence is called panicking and can be controlled by the built-in function recover.

example

package main

import (
    "fmt"
    "time"
)

func main() {
    defer fmt.Println("main defer")
    go func() {
        defer fmt.Println("goroutine defer")

        func () {
            defer fmt.Println("goroutine func defer")
            panic("err")
        }()
    }()

    time.Sleep(1 * time.Second)
}
 
// output
// goroutine func defer
// goroutine defer
// panic: err

panic 终止当前协程的正常执行流程。但一个函数F调用panic时,F未执行的操作会立即终止, panic之前声明的defer操作会正常执行,然后F返回。对于F的调用者G来说,F()等同于panic()(当然F可能在panic前还做了些别的操作), 所以G亦结束其余其他操作,执行G中之前声明的defer,以此类推。

故在上面的实例中,goroutine func defergoroutine defer均会打印,而main()中的main defer不会打印,因为main()不和panic在同一协程。

执行完defer后,panic会使整个程序退出,这一操作过程称之为panicking。panicking可以被recover控制。

recover

The recover built-in function allows a program to manage behavior of a panicking goroutine. Executing a call to recover inside a deferred function (but not any function called by it) stops the panicking sequence by restoring normal execution and retrieves the error value passed to the call of panic. If recover is called outside the deferred function it will not stop a panicking sequence. In this case, or when the goroutine is not panicking, or if the argument supplied to panic was nil, recover returns nil. Thus the return value from recover reports whether the goroutine is panicking.

example

package main

import (
    "fmt"
    "time"
)

func main() {
    defer fmt.Println("main defer")
    go func() {
        defer fmt.Println("goroutine defer")
        defer func() {
            if r := recover(); r != nil {
                fmt.Println("goroutine recover")
            }
        }()

        func () {
            defer fmt.Println("goroutine func defer ")
            panic("err")
        }()
    }()

    time.Sleep(1 * time.Second)
}

// output
// goroutine func defer
// goroutine recover
// goroutine defer
// main defer

recover允许程序控制panicking行为,但其必须在panic之前的defer中调用,因为panic会终止除了defer的一切操作。而且recocer只能控制其所在goroutine的panic,所以

defer func() {
        if r := recover(); r != nil {
            fmt.Println("goroutine recover")
        }
    }()

放置在main中的话,程序还是会以panic结束,因为panicking之前根本不会执行这个defer recover。

假设没有发生panicking的话,reciver返回一个nil。

使用场景

defer

清理资源

如上面复制文件的例子,在打开文件后就defer一个关闭文件的操作。适用于一些需要close资源的场景。

用于recover

在函数内部最顶部声明一个defer用于recover,捕捉函数内可能出现的panic,下面会介绍。

panic & recover

为什么需要panic & recover 机制?

实现 try/catch

Go有自己的错误处理机制,一般的错误会返回给调用者,但是当遇到无法处理的错误时,或者发生意料之外的错误(异常), 需要使用该机制使程序不要崩溃。这样就实现了类似其他语言中的try/catch异常处理机制。

简化错误处理

假设需要写一个业务无关的库和框架啥,难免会遇到许多error,可能会导致大量的err != nil等操作,或者说有些错误根本无法处理,这样的话,总不能使得程序崩溃吧。。,这时就需使用panic & recover来统一处理了。Go标准库中许多地方都使用了panic&recover,如json包,有兴趣可以去看。

The convention in the Go libraries is that even when a package uses panic internally, its external API still presents explicit error return values.

一个比较好的做法是,当发生panic, recover的时间,返回一个err给调用者。

func (e *encodeState) marshal(v interface{}, opts encOpts) (err error) {
    defer func() {
        if r := recover(); r != nil {
            if je, ok := r.(jsonError); ok {
                err = je.error
            } else {
                panic(r)
            }
        }
    }()
    e.reflectValue(reflect.ValueOf(v), opts)
    return nil
}

如json中的marshal,recover的时候,将err赋值返回给调用者。

原理

todo

参考:

阅读 1.2k
31 声望
1 粉丝
0 条评论
31 声望
1 粉丝
文章目录
宣传栏