golang中defer, panic, recover用法

昨天谢大在群里发了一个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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容