Go
语言中保留有传统的控制流程,比如:if
, for
, switch
,甚至连 goto
都有。另外 goroutine
语法也可以实现面向过程的多线程开发。在这里我们将要讨论一些同其它语言不相同的语句:defer
, panic
, 和 recover
defer
语句可以将一个函数放入一个链表。当调用这个 defer
语句的函数返回时,保存在链表中的所有函数将会被调用。一般情况下,defer
语句用来进行简单的各种清理工作。
例如,在一个函数中,我们打开了两个文件,并且拷贝一个文件的内容到另一个文件中:
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
}
上述函数可以正常运行,但是存在BUG。如果在调用 os.Create
时发生异常,程序这个时候并没有关闭原始被拷贝的文件。当然,我们可以在第二个 return
语句之前,添加 src.Close
,但是如果程序变化复杂,修改起来就没这样容易了。通过使用 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)
}
defer
语句可以保证打开打开文档之后,能够正确的关闭文件,而不去考虑在之后的的逻辑中有多少个 return
语句。
defer
语句的行为直观明了,并且有三个简单的特性:
deferred
函数参数初始化的时机为defer
语句声明时在下面的例子中,当
Println
被调用时,i
当前的值已经传递到函数中,所以值为0
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
- 当外围函数返回时,
deferred
函数按照 后进先出[LIFO] 的顺序执行
下面的例子将会打印3210
func b() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
}
defered
函数可以读取外围函数的返回值,并对其进行赋值操作
下面的例子中,defered
函数增加了外围函数的返回值,所以,最后的返回结果为2
func c() (i int) {
defer func() { i++ }()
return 1
}
这对于改变程序返回错误是非常方便的。在下面会举一个简单的例子。
Panic 是内置函数,终止当前的流程,使之开始变得 panicking
。例如,当函数 F
调用 panic
, F
将会停止执行,任何 deferred
函数将会正常执行,之后 F
返回到调用它的函数。对于调用者而言,F
就相当于一个 panic
。这一过程持续向上直到程序崩溃。 Panics
可以通过直接调用 panic
关键字实现,也可以通过 runtime errors
发生,例如数组越界。
Recover 也是内置函数,用来恢复遇到 panic
的 goroutine
。Recover
仅仅在 deferred
函数中才有意义。如果,在正常的函数中调用,recover
将会返回 nil
并且没有其它任何效果。如果当前的 goroutine
产生了 panic
, recover
将会捕获传递给 panic
的数据,并且恢复到正常的执行状态。
下面的程序将会说明 panic
defer
recover
的机制。
package main
import "fmt"
func main() {
f()
fmt.Println("Returned normally from f.")
}
func f() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r)
}
}()
fmt.Println("Calling g.")
g(0)
fmt.Println("Returned normally from g.")
}
func g(i int) {
if i > 3 {
fmt.Println("Panicking!")
panic(fmt.Sprintf("%v", i))
}
defer fmt.Println("Defer in g", i)
fmt.Println("Printing in g", i)
g(i + 1)
}
函数 g
根据 i
的值执行不同的动作,当 i
大于 3
时会主动产生 panic
。 函数 f
通过 deferred
函数,使用 recover
捕获异常,如果不为空,则打印恢复的数据。在继续读以下内容之前,试着想想输出的内容。
程序的输出结果为:
Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
Recovered in f 4
Returned normally from f.
如果我们将函数 f
中的 deferred
函数去掉,panic
将不会被捕获,整个程序将会崩溃。修改后的程序如下:
Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
panic: 4
panic PC=0x2a9cd8
[stack trace omitted]
可以从 Go
的标准库 json package 中获取 panic
和 recover
的真实例子。
Go
语言库的方便之处就在于,即使库种使用了 panic
,外部 API
仍然会返回明确的出错信息,并不会引起 panic
defer
的其它应用(不是之前提到的 file.Close
)包括释放锁:
mu.Lock()
defer mu.Unlock()
打印页脚
printHeader()
defer printFooter()
等等…
总而言之,defer
语句(结合 panic
和 recover
与否)提供一种不同寻常强大的控制机制。它可以通过不同多样的建模实现其它语言的特殊逻辑控制。不妨试一试哦。
By Andrew Gerrand
译者注:defer
在某种程序上更像finally
, panic
在某种程度上更像 throw exception
, recover
在某种程度上更像 catch