码迷,mamicode.com
首页 > 其他好文 > 详细

go 剖析异常

时间:2020-12-30 11:17:15      阅读:0      评论:0      收藏:0      [点我收藏+]

标签:保护   time   UNC   自己   known   返回结果   捕获异常   状态   模拟   

panic支持抛出任意类型的异常(而不仅仅是error类型的错误),recover函数调用的返回值和panic函数的输入参数类型一致,它们的函数签名如下:

func panic(interface{})
func recover() interface{}

Go语言函数调用的正常流程是函数执行返回语句返回结果,在这个流程中是没有异常的,因此在这个流程中执行recover异常捕获函数始终是返回nil。另一种是异常流程: 当函数调用panic抛出异常,函数将停止执行后续的普通语句,但是之前注册的defer函数调用仍然保证会被正常执行,然后再返回到调用者。对于当前函数的调用者,因为处理异常状态还没有被捕获,和直接调用panic函数的行为类似。在异常发生时,如果在defer中执行recover调用,它可以捕获触发panic时的参数,并且恢复到正常的执行流程。

在非defer语句中执行recover调用是初学者常犯的错误:

func main() {
    if r := recover(); r != nil {
        log.Fatal(r)
    }

    panic(123)

    if r := recover(); r != nil {
        log.Fatal(r)
    }
}

上面程序中两个recover调用都不能捕获任何异常。在第一个recover调用执行时,函数必然是在正常的非异常执行流程中,这时候recover调用将返回nil。发生异常时,第二个recover调用将没有机会被执行到,因为panic调用会导致函数马上执行已经注册defer的函数后返回。

其实recover函数调用有着更严格的要求:我们必须在defer函数中直接调用recover。如果defer中调用的是recover函数的包装函数的话,异常的捕获工作将失败!比如,有时候我们可能希望包装自己的MyRecover函数,在内部增加必要的日志信息然后再调用recover,这是错误的做法:

func main() {
    defer func() {
        // 无法捕获异常
        if r := MyRecover(); r != nil {
            fmt.Println(r)
        }
    }()
    panic(1)
}

func MyRecover() interface{} {
    log.Println("trace...")
    return recover()
}

同样,如果是在嵌套的defer函数中调用recover也将导致无法捕获异常:

func main() {
    defer func() {
        defer func() {
            // 无法捕获异常
            if r := recover(); r != nil {
                fmt.Println(r)
            }
        }()
    }()
    panic(1)
}

2层嵌套的defer函数中直接调用recover和1层defer函数中调用包装的MyRecover函数一样,都是经过了2个函数帧才到达真正的recover函数,这个时候Goroutine的对应上一级栈帧中已经没有异常信息。

如果我们直接在defer语句中调用MyRecover函数又可以正常工作了:

func MyRecover() interface{} {
    return recover()
}

func main() {
    // 可以正常捕获异常
    defer MyRecover()
    panic(1)
}

但是,如果defer语句直接调用recover函数,依然不能正常捕获异常:

func main() {
    // 无法捕获异常
    defer recover()
    panic(1)
}

必须要和有异常的栈帧只隔一个栈帧,recover函数才能正常捕获异常。换言之,recover函数捕获的是祖父一级调用函数栈帧的异常(刚好可以跨越一层defer函数)!

当然,为了避免recover调用者不能识别捕获到的异常, 应该避免用nil为参数抛出异常:

func main() {
    defer func() {
        if r := recover(); r != nil { ... }
        // 虽然总是返回nil, 但是可以恢复异常状态
    }()

    // 警告: 用`nil`为参数抛出异常
    panic(nil)
}

当希望将捕获到的异常转为错误时,如果希望忠实返回原始的信息,需要针对不同的类型分别处理:

func foo() (err error) {
    defer func() {
        if r := recover(); r != nil {
            switch x := r.(type) {
            case string:
                err = errors.New(x)
            case error:
                err = x
            default:
                err = fmt.Errorf("Unknown panic: %v", r)
            }
        }
    }()

    panic("TODO")
}

基于这个代码模板,我们甚至可以模拟出不同类型的异常。通过为定义不同类型的保护接口,我们就可以区分异常的类型了:

func main {
    defer func() {
        if r := recover(); r != nil {
            switch x := r.(type) {
            case runtime.Error:
                // 这是运行时错误类型异常
            case error:
                // 普通错误类型异常
            default:
                // 其他类型异常
            }
        }
    }()

    // ...
}

不过这样做和Go语言简单直接的编程哲学背道而驰了。

go 剖析异常

标签:保护   time   UNC   自己   known   返回结果   捕获异常   状态   模拟   

原文地址:https://www.cnblogs.com/ithubb/p/14188016.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!