www.zhblog.net

defer、panic、recover


就许多现代语言而言,异常处理机制是基本特性之一,然而,异常处理是好是坏,一直以来存在着各种不同的意见,在 Go 语言中,没有异常处理机制,取而代之的,是运用deferpanicrecover来满足类似的处理需求。

defer 延迟执行

在 Go 语言中,可以使用defer指定某个函数延迟执行,那么延迟到哪个时机?简单来说,在函数return之前,例如:

package main

import "fmt"

func deferredFunc() {
    fmt.Println("deferredFunc")    
}

func main() {
    defer deferredFunc()
    fmt.Println("Hello, 世界")    
}

这个范例执行时,deferredFunc()前加上了defer,因此,会在main()函数return前执行,结果就是先显示了"Hello, 世界",才显示"deferredFunc"

如果有多个函数被defer,那么在函数return前,会依defer的相反顺序执行,也就是 LIFO,例如:

package main

import "fmt"

func deferredFunc1() {
    fmt.Println("deferredFunc1")
}

func deferredFunc2() {
    fmt.Println("deferredFunc2")
}

func main() {
    defer deferredFunc1()
    defer deferredFunc2()
    fmt.Println("Hello, 世界")
}

由于先deferdeferredFunc1(),才deferdeferredFunc2(),因此执行结果会是"Hello, 世界""deferredFunc2""deferredFunc1"的显示顺序。

上头是为了清楚表示出defer与函数的关系,实际上,你也可以写成这样就好:

package main

import "fmt"

func main() {
    defer fmt.Println("deffered 1")    
    defer fmt.Println("deffered 2")
    fmt.Println("Hello, 世界")    
}

执行结果会是"Hello, 世界""deferred 2""deferred 1"的显示顺序。

有趣的一点是,被defer的函数若有接受某变量作为实参,那么会是被defer当时的变量值,例如:

package main

import "fmt"

func main() {
    i := 10
    defer fmt.Println(i)    
    i++
    fmt.Println(i) 
}

在上面的例子中,会显示 11 与 10,这是因为第一个fmt.Println(i)defer时,保有i当时的值 10。

使用 defer 清除资源

那么要用在何处?记得defer的特性是在函数return前执行,而且一定会被执行,因此,对于以下的这个程序:

package main

import (
    "fmt"
    "os"
)

func main() {
    f, err := os.Open("/tmp/dat")
    if err != nil {
        fmt.Println(err)
    } else {
        b1 := make([]byte, 5)
        n1, err := f.Read(b1)
        if err != nil {
            fmt.Println(err)
        } else {
            fmt.Printf("%d bytes: %s\n", n1, string(b1))
            // 处理读取的内容....
            f.Close()
        }
    }
}

这是一个读取文件的例子,os.Openf.Read的风格是返回两个值,第二个值代表着有无错误发生,因此程序中进行了错误的检查,在没有错误的情况下才进行文件的读取与内容处理,而最后透过f.Close()关闭文件。

基本上,这个范例的问题在于,f.Close()不一定会被执行,因为 Go 语言中还有其他展现错误的方式,例如使用panic函数。假设在「处理读取的内容」过程中因为调用了panic来表示有错误发生,那么会立即中断函数的执行(在这个例子就是直接离开main函数),这时f.Close()就不会被执行。

你可以使用defer来执行函数的关闭:

package main

import (
    "fmt"
    "os"
)

func main() {
    f, err := os.Open("/tmp/dat")
    if err != nil {
        fmt.Println(err)
        return;
    }

    defer func() { // 延迟执行,而且函数 return 前一定会执行
        if f != nil {
            f.Close()
        }
    }()

    b1 := make([]byte, 5)
    n1, err := f.Read(b1)
    if err != nil {
        fmt.Printf("%d bytes: %s\n", n1, string(b1))
        // 处理读取的内容....
    }
}

这么一来,若Read发生错误,最后一定会执行被defer的函数,从而保证了f.Close()一定会关闭文件。

(就某些意义来说,defer的角色类似于异常处理机制中finally的机制,将资源清除的函数,借由defer来处理,一方面大概也是为了在代码阅读上,强调出资源清除的重要性吧!)

panic 恐慌中断

方才稍微提过,如果在函数中执行panic,那么函数的流程就会中断,若 A 函数调用了 B 函数,而 B 函数中调用了panic,那么 B 函数会从调用了panic的地方中断,而 A 函数也会从调用了 B 函数的地方中断,若有更深层的调用链,panic的效应也会一路往返回播。

(如果你有异常处理的经验,这就相当于被抛出的异常都没有处理的情况。)

可以将方才的范例改写为以下:

package main

import (
    "fmt"
    "os"
)

func check(err error) {
    if err != nil {
        panic(err)
    }
}

func main() {
    f, err := os.Open("/tmp/dat")
    check(err)

    defer func() {
        if f != nil {
            f.Close()
        }
    }()

    b1 := make([]byte, 5)
    n1, err := f.Read(b1)
    check(err)

    fmt.Printf("%d bytes: %s\n", n1, string(b1))
}

如果在开启文件时,就发生了错误,假设这是在一个很深的调用层次中发生,若你直接想编写程序,将os.Openerror逐层返回,那会是一件很麻烦的事,此时直接发出panic,就可以达到想要的目的。

recover 恢复流程

如果发生了panic,而你必须做一些处理,可以使用recover,这个函数必须在被defer的函数中执行才有效果,若在被defer的函数外执行,recover一定是返回nil

如果有设置defer函数,在发生了panic的情况下,被defer的函数一定会被执行,若当中执行了recover,那么panic就会被捕捉并作为recover的返回值,那么panic就不会一路往返回播,除非你又调用了panic

因此,虽然 Go 语言中没有异常处理机制,也可使用deferpanicrecover来进行类似的错误处理。例如,将上头的范例,再修改为:

package main

import (
    "fmt"
    "os"
)

func check(err error) {
    if err != nil {
        panic(err)
    }
}

func main() {
    f, err := os.Open("/tmp/dat")
    check(err)

    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err) // 这已经是顶层的 UI 接口了,想以自己的方式呈现错误
        }

        if f != nil {
            if err := f.Close(); err != nil {
                panic(err) // 示范再抛出 panic
            }
        }
    }()

    b1 := make([]byte, 5)
    n1, err := f.Read(b1)
    check(err)

    fmt.Printf("%d bytes: %s\n", n1, string(b1))
}

在这个例子中,假设已经是最顶层的 UI 接口了,因此使用recover尝试捕捉panic,并以自己的方式呈现错误,附带一题的是,关闭文件也有可能发生错误,程序中也检查了f.Close(),视需求而定,你可以像这边重新抛出panic,或者也可以单纯地设计一个 UI 接口来呈现错误。

什么时候该用error?什么时候该用panic?在 Go 的惯例中,鼓励你使用error,明确地进行错误检查,然而,就如方才所言,嵌套且深层的调用时,使用panic会比较便于传播错误,就 Go 的惯例来说,是以包为界限,于包之中,必要时可以使用panic,而包公开的函数,建议以error来回报错误,若包公开的函数可能会收到panic,建议使用recover捕捉,并转换为error


展开阅读全文

评论

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 心情