zoukankan      html  css  js  c++  java
  • go 剖析异常

    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语言简单直接的编程哲学背道而驰了。

  • 相关阅读:
    直道相思了无益 你既无心我便休
    c#与XML
    ASP.NET读取Excel文件的三大方法浅析
    当前标识(NT AUTHORITY\NETWORK SERVICE)没有对“C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files”的写访问权限。
    比较这2段HTML代码
    编码的一点思维
    代码修改的一个范例
    在aspx.cs中不出现中文?
    规则先行
    设计模式——UML简介
  • 原文地址:https://www.cnblogs.com/ithubb/p/14188016.html
Copyright © 2011-2022 走看看