• go语言的panic和recover函数


    错误和异常是两个不同的概念,非常容易混淆。很多程序员习惯将一切非正常情况都看做错误,而不区分错误和异常,即使程序中可能有异常抛出,也将异常及时捕获并转换成错误。从表面上看,一切皆错误的思路更简单,而异常的引入仅仅增加了额外的复杂度。

    但事实并非如此。众所周知,Golang遵循“少即是多”的设计哲学,追求简洁优雅,就是说如果异常价值不大,就不会将异常加入到语言特性中。

    错误和异常处理是程序的重要组成部分,我们先看看下面几个问题:

    1. 错误和异常如何区分?
    2. 错误处理的方式有哪几种?
    3. 什么时候需要使用异常终止程序?
    4. 什么时候需要捕获异常?

     

    错误指的是可能出现问题的地方出现了问题,比如打开一个文件时失败,这种情况在人们的意料之中 ;而异常指的是不应该出现问题的地方出现了问题,比如引用了空指针,这种情况在人们的意料之外。可见,错误是业务过程的一部分,而异常不是 。

    Golang中引入error接口类型作为错误处理的标准模式,如果函数要返回错误,则返回值类型列表中肯定包含error。error处理过程类似于C语言中的错误码,可逐层返回,直到被处理。

    Golang中引入两个内置函数panic和recover来触发和终止异常处理流程,同时引入关键字defer来延迟执行defer后面的函数。

    一直等到包含defer语句的函数执行完毕时,延迟函数(defer后的函数)才会被执行,而不管包含defer语句的函数是通过return的正常结束,还是由于panic导致的异常结束。你可以在一个函数中执行多条defer语句,它们的执行顺序与声明顺序相反。

    当程序运行时,如果遇到引用空指针、下标越界或显式调用panic函数等情况,则先触发panic函数的执行,然后调用延迟函数。调用者继续传递panic,因此该过程一直在调用栈中重复发生:函数停止执行,调用延迟执行函数等。如果一路在延迟函数中没有recover函数的调用,则会到达该携程的起点,该携程结束,然后终止其他所有携程,包括主携程(类似于C语言中的主线程,该携程ID为1)。

    错误和异常从Golang机制上讲,就是error和panic的区别。很多其他语言也一样,比如C++/Java,没有error但有errno,没有panic但有throw。

    Golang错误和异常是可以互相转换的:

    1. 错误转异常,比如程序逻辑上尝试请求某个URL,最多尝试三次,尝试三次的过程中请求失败是错误,尝试完第三次还不成功的话,失败就被提升为异常了。
    2. 异常转错误,比如panic触发的异常被recover恢复后,将返回值中error类型的变量进行赋值,以便上层函数继续走错误处理流程。

     

    什么情况下用错误表达,什么情况下用异常表达,就得有一套规则,否则很容易出现一切皆错误或一切皆异常的情况。

     

    在这个启示下,我们给出异常处理的作用域(场景):

    1. 空指针引用
    2. 下标越界
    3. 除数为0
    4. 不应该出现的分支,比如default
    5. 输入不应该引起函数错误

     

    其他场景我们使用错误处理,这使得我们的函数接口很精炼。对于异常,我们可以选择在一个合适的上游去recover,并打印堆栈信息,使得部署后的程序不会终止。

     

    说明: Golang错误处理方式一直是很多人诟病的地方,有些人吐槽说一半的代码都是"if err != nil { / 打印 && 错误处理 / }",严重影响正常的处理逻辑。当我们区分错误和异常,根据规则设计函数,就会大大提高可读性和可维护性。

     

    错误处理的正确姿势

     

    1. 失败的原因只有一个时,不使用error

     
    func (self *AgentContext) CheckHostType(host_type string) error {
        switch host_type {
        case "virtual_machine":
            return nil
        case "bare_metal":
            return nil
        }
        return errors.New("CheckHostType ERROR:" + host_type)
    }

    我们可以看出,该函数失败的原因只有一个,所以返回值的类型应该为bool,而不是error,重构一下代码:

     
    func (self *AgentContext) IsValidHostType(hostType string) bool {
        return hostType == "virtual_machine" || hostType == "bare_metal"
    }

    说明:大多数情况,导致失败的原因不止一种,尤其是对I/O操作而言,用户需要了解更多的错误信息,这时的返回值类型不再是简单的bool,而是error。

     

    2. 没有失败时,不使用error

    3. error应放在返回值类型列表的最后

    4. 错误值统一定义,而不是跟着感觉走

    很多人写代码时,到处return errors.New(value),而错误value在表达同一个含义时也可能形式不同,比如“记录不存在”的错误value可能为:

    1. "record is not existed."
    2. "record is not exist!"
    3. "###record is not existed!!!"
    4. ...

    这使得相同的错误value撒在一大片代码里,当上层函数要对特定错误value进行统一处理时,需要漫游所有下层代码,以保证错误value统一,不幸的是有时会有漏网之鱼,而且这种方式严重阻碍了错误value的重构。

    于是,我们可以参考C/C++的错误码定义文件,在Golang的每个包中增加一个错误对象定义文件,如下所示:

     

    5. 错误逐层传递时,层层都加日志

    根据笔者经验,层层都加日志非常方便故障定位。

    说明:至于通过测试来发现故障,而不是日志,目前很多团队还很难做到。如果你或你的团队能做到,那么请忽略这个姿势:)

    6. 错误处理使用defer

    我们一般通过判断error的值来处理错误,如果当前操作失败,需要将本函数中已经create的资源destroy掉,示例代码如下:

     

    当Golang的代码执行时,如果遇到defer的闭包调用,则压入堆栈。当函数返回时,会按照后进先出的顺序调用闭包。

    对于闭包的参数是值传递,而对于外部变量却是引用传递,所以闭包中的外部变量err的值就变成外部函数返回时最新的err值。

    根据这个结论,我们重构上面的示例代码:

     

    7. 当发生错误时,不忽略有用的返回值

    通常,当函数返回non-nil的error时,其他的返回值是未定义的(undefined),这些未定义的返回值应该被忽略。然而,有少部分函数在发生错误时,仍然会返回一些有用的返回值。比如,当读取文件发生错误时,Read函数会返回可以读取的字节数以及错误信息。对于这种情况,应该将读取到的字符串和错误信息一起打印出来。

     

    对待异常的态度

    1. 在程序开发阶段,坚持速错

    去年学习Erlang的时候,建立了速错的理念,简单来讲就是“让它挂”,只有挂了你才会第一时间知道错误。在早期开发以及任何发布阶段之前,最简单的同时也可能是最好的方法是调用panic函数来中断程序的执行以强制发生错误,使得该错误不会被忽略,因而能够被尽快修复。

     

    2. 在程序部署后,应恢复异常避免程序终止

    我们在调用recover的延迟函数中以最合理的方式响应该异常:

    1. 打印堆栈的异常调用信息和关键的业务信息,以便这些问题保留可见;
    2. 将异常转换为错误,以便调用者让程序恢复到健康状态并继续安全运行。

    我们看一个简单的例子:

     

    我们期望test函数的输出是:

     

    实际上test函数的输出是:

     

    原因是panic异常处理机制不会自动将错误信息传递给error,所以要在funcA函数中进行显式的传递,代码如下所示:

     

     

    3. 对于不应该出现的分支,使用异常处理

    当某些不应该发生的场景发生时,我们就应该调用panic函数来触发异常。比如,当程序到达了某条逻辑上不可能到达的路径:

     

    4. 针对入参不应该有问题的函数,使用panic设计

    入参不应该有问题一般指的是硬编码,我们先看“一个启示”一节中提到的两个函数(Compile和MustCompile),其中MustCompile函数是对Compile函数的包装:

     

    所以,对于同时支持用户输入场景和硬编码场景的情况,一般支持硬编码场景的函数是对支持用户输入场景函数的包装。

    对于只支持硬编码单一场景的情况,函数设计时直接使用panic,即返回值类型列表中不会有error,这使得函数的调用处理非常方便(没有了乏味的"if err != nil {/ 打印 && 错误处理 /}"代码块)。

    5. 测试代码

    package main
    
    import (
        "fmt"
    )
    
    func div(a, b int) (int, error) {
        if b == 0 {
            //自己定义报错信息
            panic("被除数不能为0")
        }
        return a / b, nil
    }
    
    func main() {
        //错误就是能遇到可能出现的情况,这些情况会导致你的代码出问题, 参数检查, 数据库访问不了
        a := 12
        b := 0
        defer func() {
            err := recover()
            if err != nil {
                fmt.Println("异常被捕获到")
            }
            fmt.Println("bobby")
        }()
    
        fmt.Println(div(a, b))
    
        //panic的坑
    
        //strconv.Itoa(data) //go 认为这个Itoa的函数不可能出错, 没有必要返回error 内部代码出错这个时候应该抛出异常 panic
        //_, err := strconv.Atoi("abcd") //Atoi这个函数认为我的函数内部会出现一些预知错误情况
        //if err != nil {
        //    //错误
        //}
    
        //异常 go语言中如何抛出异常和如何捕捉异常
        //go语言认为错误就要自己处理, 就个人而言,go语言的这种想法是正确的。但是实际的使用中确实人有点烦人
    }

     

     
  • 相关阅读:
    2.8 Classes of Restricted Estimators
    navicat远程登录windows服务器
    面试题
    【南阳OJ分类之语言入门】80题题目+AC代码汇总
    基于‘BOSS直聘招聘信息’分析企业到底需要什么样的PHPer
    数据开源
    Pyhton爬虫实战
    Python爬虫框架Scrapy实战
    做网站用UTF-8编码还是GB2312编码?
    【南阳OJ分类之大数问题】题目+AC代码汇总
  • 原文地址:https://www.cnblogs.com/wlike/p/16411831.html
Copyright © 2020-2023  润新知