就许多现代语言而言,异常处理机制是基本特性之一,然而,异常处理是好是坏,一直以来存在着各种不同的意见,在 Go 语言中,没有异常处理机制,取而代之的,是运用defer
、panic
、recover
来满足类似的处理需求。
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, 世界")
}
由于先defer
了deferredFunc1()
,才defer
了deferredFunc2()
,因此执行结果会是"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.Open
与f.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.Open
的error
逐层返回,那会是一件很麻烦的事,此时直接发出panic
,就可以达到想要的目的。
recover 恢复流程
如果发生了panic
,而你必须做一些处理,可以使用recover
,这个函数必须在被defer
的函数中执行才有效果,若在被defer
的函数外执行,recover
一定是返回nil
。
如果有设置defer
函数,在发生了panic
的情况下,被defer
的函数一定会被执行,若当中执行了recover
,那么panic
就会被捕捉并作为recover
的返回值,那么panic
就不会一路往返回播,除非你又调用了panic
。
因此,虽然 Go 语言中没有异常处理机制,也可使用defer
、panic
与recover
来进行类似的错误处理。例如,将上头的范例,再修改为:
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
。