Golang 学习笔记(07)—— 错误及异常处理

基础知识

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

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

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

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

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

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

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

package main

import ( "errors" "fmt" )

func main(){ defer func(){ if err := recover(); err != nil{ //捕捉异常并处理 fmt.Println("err: ", err) } }()

if num, err := delive(20, -5); err == nil{
    fmt.Printf("%f / %f = %f\n", 20.0, -5.0, num)
}else{
    fmt.Println(err)
}

if num, err := delive(20, 0); err == nil{
    fmt.Printf("%f / %f = %f\n", 20.0, 0.0, num)
}else{
    fmt.Println(err)
}
复制代码

}

func delive(numA, numB float32) (float32, error){ if numB < 0{ return 0, errors.New("被除数不能为负数") }else if numB == 0{ panic("被除数不能为0") //抛出异常 }else{ return numA/numB, nil } } 运行结果 error 类型实际上是抽象了 Error() 方法的 error 接口,Golang 使用该接口进行标准的错误处理。 Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理。 error使用场景

失败原因只有一个时,不应该使用error 当函数失败的原因只有一个,所以返回值的类型应该为bool,而不是error。大多数情况,导致失败的原因不止一种,尤其是对I/O操作而言,用户需要了解更多的错误信息,这时的返回值类型不再是简单的bool,而是error。

没有失败时,不使用error error在Golang中是如此的流行,以至于很多人设计函数时不管三七二十一都使用error,即使没有一个失败原因。当没有失败原因时,使用无返回的函数会更加合理。

error应放在返回值类型列表的最后 对于返回值类型error,用来传递错误信息,在Golang中通常放在最后一个。bool作为返回值类型时也一样。

错误值统一定义,而不是跟着感觉走 很多人写代码时,到处return errors.New(value),而错误value在表达同一个含义时也可能形式不同。 这使得相同的错误value撒在一大片代码里,当上层函数要对特定错误value进行统一处理时,需要漫游所有下层代码,以保证错误value统一,不幸的是有时会有漏网之鱼,而且这种方式严重阻碍了错误value的重构。 于是,我们可以在Golang的每个包中增加一个错误对象定义文件,如下所示:

var ERR_EOF = errors.New("EOF") var ERR_CLOSED_PIPE = errors.New("io: read/write on closed pipe") var ERR_NO_PROGRESS = errors.New("multiple Read calls return no data or error") var ERR_SHORT_BUFFER = errors.New("short buffer") var ERR_SHORT_WRITE = errors.New("short write") var ERR_UNEXPECTED_EOF = errors.New("unexpected EOF") 错误逐层传递时,层层都加日志 层层都加日志非常方便故障定位。

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

func deferDemo() error { err := createResource1() if err != nil { return ERR_CREATE_RESOURCE1_FAILED } err = createResource2() if err != nil { destroyResource1() return ERR_CREATE_RESOURCE2_FAILED }

err = createResource3()
if err != nil {
    destroyResource1()
    destroyResource2()
    return ERR_CREATE_RESOURCE3_FAILED
}

err = createResource4()
if err != nil {
    destroyResource1()
    destroyResource2()
    destroyResource3()
    return ERR_CREATE_RESOURCE4_FAILED
}
return nil
复制代码

} 当Golang的代码执行时,如果遇到defer的闭包调用,则压入堆栈。当函数返回时,会按照后进先出的顺序调用闭包。 对于闭包的参数是值传递,而对于外部变量却是引用传递,所以闭包中的外部变量err的值就变成外部函数返回时最新的err值。 根据这个结论,我们重构上面的示例代码:

func deferDemo() error { err := createResource1() if err != nil { return ERR_CREATE_RESOURCE1_FAILED } defer func() { if err != nil { destroyResource1() } }() err = createResource2() if err != nil { return ERR_CREATE_RESOURCE2_FAILED } defer func() { if err != nil { destroyResource2() } }()

err = createResource3()
if err != nil {
    return ERR_CREATE_RESOURCE3_FAILED
}
defer func() {
    if err != nil {
        destroyResource3()
    }
}()

err = createResource4()
if err != nil {
    return ERR_CREATE_RESOURCE4_FAILED
}
return nil
复制代码

} 当上层函数不关心错误时,建议不返回error 对于一些资源清理相关的函数(destroy/delete/clear),如果子函数出错,打印日志即可,而无需将错误进一步反馈到上层函数,因为一般情况下,上层函数是不关心执行结果的,或者即使关心也无能为力,于是我们建议将相关函数设计为不返回error。 异常的使用场景

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

在程序部署后,应恢复异常避免程序终止 一旦Golang程序部署后,在任何情况下发生的异常都不应该导致程序异常退出,我们在上层函数中加一个延迟执行的recover调用来达到这个目的,并且是否进行recover需要根据环境变量或配置文件来定,默认需要recover。 我们在调用recover的延迟函数中以最合理的方式响应该异常:

打印堆栈的异常调用信息和关键的业务信息,以便这些问题保留可见; 将异常转换为错误,以便调用者让程序恢复到健康状态并继续安全运行。 对于不应该出现的分支,使用异常处理 当某些不应该发生的场景发生时,我们就应该调用panic函数来触发异常。比如,当程序到达了某条逻辑上不可能到达的路径

针对入参不应该有问题的函数,使用panic设计 对于同时支持用户输入场景和硬编码场景的情况,一般支持硬编码场景的函数是对支持用户输入场景函数的包装。 对于只支持硬编码单一场景的情况,函数设计时直接使用panic,即返回值类型列表中不会有error,这使得函数的调用处理非常方便

作者:ChainZhang 链接:https://www.jianshu.com/p/9447afd72189 來源:简书 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

转载于:https://juejin.im/post/5a6d97c2f265da3e283a40e8

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值