【GoLang那点事】深入Go的异常错误处理机制(二)

开篇词

  • 上一篇文章分享了Go的异常,错误处理使用,未读过的可以点击回顾一下,我们知道程序运行中,有异常,有错误,那么什么是异常,什么是错误,和其他语言相比,Go的异常错误机制有什么优点,缺点?我们如何更好的理解,如何用Go写出更健壮的程序,今天来聊一聊这些问题。

异常和错误

  • 关于异常和错误每个人都有自己的理解,很多人往往把这个混为一谈,认为他们是等价的,这里我们从Java 和Go两种语言异常错误体系的设计分析来试图回答这个问题。
  • Java中,Throwable是所有错误(Error)和异常 (Exception) 的基类,整的来说,它们都是程序运行过程中可能出现的问题,区别在哪里呢? Exception是可预料的,而Error是不可预料的,举个例子,工程队要盖一栋楼,我们把盖这栋楼的过程比作程序一段运行的过程,在盖楼的过程中,建筑人员对于对于瓷砖损坏,下雨,断电,设计错误等都是在意料之内的,并且在发生这些问题时是由恢复,补救措施的,而对于地震,地面塌陷,狂风这些问题是不可预知,意料之外的,并且这些问题真的发生了,也是无法补救,恢复的,只能重新来过。Exception我理解为在程序运行中正常情况下意料之中发生的事,是可以被程序员处理,补救,有机会回到正常处理流程的,而Error在程序运行中非正常成矿下发生后是无法被处理,恢复,比如内存溢出,栈溢出等。
  • Go的异常错误设计体系只有Error,任何一切都在方法返回值中返回可能发生的错误,那么go有没有运行过程中意料之外的错误呢,答案是有呢,panic和defer以及recover共同组成了这个体系,但这个体系最终还是被返回Error所处理,什么含义呢,就是在意料之外的panic发生时,在defer中通过recover捕获这个恐慌,转化为错误通过方法返回值告诉方法调用者,看到这里,其实从字面意思,Go中弱化了异常,一切皆错误,都被包装成类似Code,Message的形式返回方法调用者,直到有调用者去处理, 这也是Go的设计精髓,简化没必要存在的。

从编码上看看Go和Java的异常设计思想

  • 如果你看过Go的许多代码,那么 if err != ni, 应该随处可见,尤其是业务代码开发中,是的,Go没有类似 try catch 这样的语句,Go对错误的价值观是可编程,我们看下面的代码:

result1,err := func1()
if err != nil{
    return nil,,err
}
result2,err := func2()
if err != nil{
    return nil,err
}
result3,err := func3()
if err != nil{
    return nil,err
}
return result1+result2+result3,nil


try{
    result1 = method1();
    reuslt2 = method2();
    result3 = method3();
    return result1+result2+result3;
}cache(Exception e){
    log.error(e);
    return null;
}
  • 上面的代码我想了解Java和Go的人都知道,我们从多个方面来看看上面的代码,第一:从顺序角度来看,是否Go更能被理解,每一个方法都有一个结果值和一个可能发生的错误值,而Java需要更多的语法,意味着需要更多理解,思考;第二:从对异常错误处理角度来看,Go中程序员对err有更多的操作空间,有更多的可编程性,而Java中相对可编程性弱化了许多;第三:也是最直观的,代码量我们发现Go的代码量比Java多了将近一半,而这种代码并没有什么技术量,重复的代码到处都是,是的,这也是许多开发者对Go不满意的地方,但这种是可以通过开发者代码设计去规避的,这里暂且不讨论

    • *
  • Java中通过throw new 抛出一个异常,通过 try cache捕获,而Go中通过panic抛出一个恐慌,通过defer和recover来处理,我们来看看代码,在分析

func test() (err error){
    defer func(){
        if e:=recover(); e != nil{
             err = e.(error)
        }
    }()    
    panic("发生恐慌了")
    return err
}


publis Result test(){
    try{
        throw new RuntimeException("发生异常了")
    }cache(Exception e){
        //处理错误
        1 打印错误,忽略
        2 throw e,继续抛出
        //转化为code,message
        3 new Result(code,e.message) 
    }
}
  • 我们分析上面两段代码,当异常或者恐慌发生时,我们可以看到Go中在defer里对通过recover捕获panic,将其转化为一个错误,通过返回值的形式返回,而在Java中异常发生时,捕获以后处理方式为要么打印,要么throw出去抛给上层的方法调用者,站在方法全局来看,当你是一个调用者时,你期望的是什么?如果你有接口交互的开发经验,我想你不会给调用接口的人抛出一个exception 或者 panic,他会不高兴的,同样你也不希望接口返回的是一堆堆栈信息,那么 在上面Java的cache中最终是返回一个Result,包含code,message,这样去看,我们对Java异常包装后是否和Go的错误设计有异曲同工之妙,同样的Go通过panic和defer,recover也可以为try,cache, throw这样的处理,但语言层面的设计的本质是不一样的(记住哦),相互的转化只是人为的在包装而已,请不要偏离正轨哦。

聊聊exception和panic给Java和Go带来了什么

  • 不管是Go还是Java,我们知道当程序启动后,对操作系统而言都是一个进程,Java中,在一个进程中可以启动多个线程,线程是Java的最小单位,Go中,一个进程中依然会有多个线程,但这不是最小单位,协程是Go的最小单位;
  • Exception在Java中的作用域是当前线程,也就是说当Exception中发生时,只会影响到当前线程的执行,终止的是当前线程。
  • Panic在Go中的作用域是整个进程,当Panic发生时,如果当前协程没捕获,则整个Go的进程就会终止,这是非常可怕的。
  • 所以需要开发人员在go的错误处理时需要谨慎,需要手工处理所有的err,尤其在对panic可能发生的地方需要捕获,这稍微增加了开发人员的心智负担
  • 同样的,我们能看到Go的程序需要更多的严谨性,健壮性,所以在开发阶段,快速试错,让尽可能多的错误立刻出现,然后修复。
  • 提醒的是,尤其是在设计一些底层框架,方法时,一定需要对panic处理,转化为err返回,否则框架抛出的panic会导致整个Go的程序结束

总结

  • 在我看来,并没有绝对,Java中对异常和错误有一个比较清晰的边界,通过类继承体系进行隔离,错误并不在程序员的考虑范围之内,通过异常体系和控制流程来实现业务逻辑,往往也容易被滥用;而Go中并没有,且弱化了异常的概念,并提供了将异常转化为错误的方法。一切皆错误,拥有更好的可编程性,但同时也带来诸如 if err != nil的这样的代码到处都是,不同的编程语言对异常错误体系设计不一样,也代表不同开发者的思想,没有对与错,个人认为都能解决特定的问题,同时也会带来一定的困扰,一定要理解这种异常错误体系设计在当前编程语言中的设计思想,才能更好的使用,写出更优雅的代码。

Go认为:

  • 让程序员更直接的接触错误,从而处理
  • 错误是一种可编程的值
  • 强调的是,无论何时,检查错误都是至关重要的,而不是如何避免检查错误

事实上关于Go的错误处理也有一些最佳实践,有自己的思想,后续我也会整理,就不再这边文章中发布了,大家可以看看下面链接的文章

  1. https://blog.golang.org/errors-are-values
  2. https://blog.golang.org/error-handling-and-go
  3. https://blog.golang.org/defer-panic-and-recover

欢迎大家关注微信公众号:“golang那点事”,更多精彩期待你的到来

0 个评论

要回复文章请先登录注册