golang中defer, panic, recover用法

2017-07-20
7分钟阅读时长

昨天谢大在群里发了一个golang面试题, 第一题就不会做了. 这题主要是考察defer, panic, 于是各种谷歌, 就写下了这篇文章, 由于本人水平有限, 有哪些理解不到的地方, 请在下面留言指出

一. defer 用法

为何会有defer这样的语法呢? 如果你之前是写C++的话这样的代码, 你会经常看到.

class Demo {
public:
	Demo() {
		p = new int(10);
	}
	
	~Demo() {
		if (p) { delete(p); }
	}
private:
	int *p = nullptr;
}

本来就是想要简单使用某个变量(比如, new出来的变量,文件句柄, mutex等等), 如果程序写的简单的话, 我们一般都会记得去释放这些变量, 但是程序会越写越复杂, 再加上各种函数之间的传递, 如果稍微不注意去释放这些变量, 内存泄漏就出来了(一般c/c++的BUG都是由这个引起的). 这个时候我们就会利用c++的析构函数, 去自动释放这些变量. 在c++11, boost专门为了这玩意制定出了智能指针, 说实话, 这玩意真心么有那么好用.

在初接触到go时, 就被defer吸引住了, 要是c++也能这么写, 那就太爽了!

defer的特性: 在函数返回之前, 调用defer函数的操作, 简化函数的清理工作.

使用defer关键字的时候, 有下面这些注意点:

1. 在defer表达式确定的时候, defer修饰的函数(后面统称为defered函数)的参数也就确定了

func argument() {
	i := 0
	defer fmt.Println(i)
	i++
	return
}

2. 函数内可以有多个defered函数, 但是这些defered函数在函数返回时遵守后进先出的原则

func LIFO() {
	for i := 0; i<4; i++ {
		defer fmt.Print(i)
	}
}

3. 函数命名的返回值跟defered函数一起使用 函数的返回值有可能被defer更改, 本质原因是return xxx语句并不是一条原子指令

func f() (result int) {
    defer func() {
        result++
    }()
    return 0
}
func g() (r int) {
     t := 5
     defer func() {
       t = t + 5
     }()
     return t
}
func h() (r int) {
    defer func(r int) {
          r = r + 5
    }(r)
    return 1
}

对于defered函数跟函数命名返回值一块使用的情况, 当无法判断返回值的时候, 需要对函数进行变形.

func f(result int) {
	result = 0
	func () {
		result++
	}()
	return
} 

结果: 1

func g() (r int) {
	t := 5
	r = t
	func () {
		t = t + 5
	}
	return
}

结果: 5

func h() (r int) {
	r = 1
	func (r int) {
	 	r = r + 5
	}(r)
	return
}

结果: 1, 在func(r int) {...}中, 由于r是以值传递的方式进行的, 所以r的值不会改变.

defer涉及到所有的代码, 点击这里查看

关于defer实现原理, 留到后面出个专题

二. panic用法

Panic is a built-in function that stops the ordinary flow of control and begins panicking. When the function F calls panic, execution of F stops, any deferred functions in F are executed normally, and then F returns to its caller. To the caller, F then behaves like a call to panic. The process continues up the stack until all functions in the current goroutine have returned, at which point the program crashes. Panics can be initiated by invoking panic directly. They can also be caused by runtime errors, such as out-of-bounds array accesses.

panic用法挺简单的, 上面这段引用是golang的官方说法. panic其实就是c++中的throw exception

panic 是内建函数.panic会中断函数F的正常执行流程, 从F函数中跳出来, 跳回到F函数的调用者. 对于调用者来说, F看起来就是一个panic, 所以调用者会继续向上跳出, 直到当前goroutine返回. 在跳出的过程中, 进程会保持这个函数栈. 当goroutine退出时, 程序会crash.

要注意的是, F函数中的defered函数会正常执行, 按照上面defer的规则.

同时引起panic除了我们主动调用panic之外, 其他的任何运行时错误, 例如数组越界都会造成panic

看下面一个例子

package main

import (
	"fmt"
)

func main() {
	defer_call()
}

func defer_call() {
	defer func() { fmt.Println("打印前") }()
	defer func() { fmt.Println("打印中") }()
	defer func() { fmt.Println("打印后") }()
	panic("触发异常")
	fmt.Println("test")
}

结果:

打印后
打印中
打印前
panic: 触发异常
goroutine 1 [running]:
main.defer_call()
	/Users/wuling/go/src/github.com/georgehao/test/panic.go:15 +0xc0
main.main()
	/Users/wuling/go/src/github.com/georgehao/test/panic.go:8 +0x20

由这个例子, 我们看到程序没有打印test, 这就说明触发panic的函数defer_callpanic("触发异常")跳了出来. 先执行defered函数, 然后抛出panic异常信息. defered还是按照FILO的规则调用. (使用Gogland IDE的同学要注意了, Gogland返回的panic异常信息跟defered函数的打印是无顺序的, 可能是Gogland的BUG)

下面对上面的例子做下稍微改动

package main

import (
	"fmt"
)

func main() {
	go defer_call()
}

func defer_call() {
	defer func() { fmt.Println("打印前") }()
	defer func() { fmt.Println("打印中") }()
	defer func() { fmt.Println("打印后") }()
	panic("触发异常")
	fmt.Println("test")
}

我们看程序结果: 没有任何输出, 而且程序也没有crash, 那是不是我们说了那么多, 是错了呢. 不, 我们再仔细想想, 是这为什么呢?

是由于main``goroutine没有等defer_call goroutine返回就程序就结束了,程序当然不会crash. 在go defer_call()下再加一行time.Sleep(time.Minute)是不是效果就不一样了.

其实想说的不是这个, 想说的是panic只会让当前的goroutine返回. 如果当前的goroutine没有去捕获这个panic的话, 那么程序就会crash.

三. recover 用法

Recover is a built-in function that regains control of a panicking goroutine. Recover is only useful inside deferred functions. During normal execution, a call to recover will return nil and have no other effect. If the current goroutine is panicking, a call to recover will capture the value given to panic and resume normal execution.

recover也是一个内建函数. recover就是c++中的catch.

不过需要注意的是:

  1. recover如果想起作用的话, 必须在defered函数中使用.
  2. 在正常函数执行过程中, 调用recover没有任何作用, 他会返回nil. 如这样:fmt.Println(recover()) // nil
  3. 如果当前的goroutine panic了, 那么recover将会捕获这个panic的值, 并且让程序正常执行下去, 不会让程序crash.

举个栗子:

package main

import "fmt"

func recoverPanic() {
	func () {
		if r := recover(); r != nil {
			fmt.Println("recover value is", r)
		}
	}()

	panic("exception")
}

func main()  {
	recoverPanic()
}

这段代码有什么问题吗? 其实要是这么写的, recover没有任何作用, 因为recover必须在defered函数中才有作用. 点击这里查看正确的代码

四. 综合的例子

官方的一个例子, 基本就是defer, panic, 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)
}

程序输出:

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.

这里有个问题, 为什么 fmt.Println("Returned normally from g.")没有打印, 而fmt.Println("Returned normally from f.")打印了呢?

先看下面这个例子, 只是对上面的例子, 移动了defered recover函数的位置:

package main

import "fmt"

func main() {
    f()
    fmt.Println("Returned normally from f.")
}

func f() {
    fmt.Println("Calling g.")
    g(0)
    fmt.Println("Returned normally from g.")
}

func g(i int) {
	defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()
    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)
}

是不是输出结果不一样了.

Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Recovered in f 4
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
Returned normally from g.
Returned normally from f.

由此, 我们可以看出, 在当前goroutine的中, recover会捕获recover所在的函数产生的的panic, 由于panic会让当前函数返回, 但是对于其调用者来说, 这个panic已经不存在了, 所以程序还是会按照正常的执行流程执行下去. 所以这个例子会打印出来 fmt.Println("Returned normally from g."), 而上面的那个却不会.

参考链接

  1. golang面试题
  2. Defer, Panic, and Recover'
  3. defer关键字
  4. Golang中defer、return、返回值之间执行顺序的坑
  5. build-web-application-with-golang