GO语言
01GO基础-001GO语言简介
01GO基础-002语言环境安装
01GO基础-003Go 语言结构
01GO基础-004Go 语言基础语法1
01GO基础-004Go 语言基础语法2
01GO基础-004Go 语言基础语法3
01GO基础-005Go 语言数据类型
01GO基础-006Go 语言变量
01GO基础-007Go 语言常量
01GO基础-008Go 语言运算符
01GO基础-009条件语句
01GO基础-010循环语句
01GO基础-011函数
01GO基础-012变量作用域
01GO基础-013数组
01GO基础-014指针
01GO基础-015结构体
01GO基础-016切片
01GO基础-017范围(Range)
01GO基础-018Map
01GO基础-019递归函数
01GO基础-020类型转换
01GO基础-021接口
01GO基础-022异常处理
01GO基础-023并发
01GO基础-024strings
01GO基础-025可变参数
01GO基础-026接口2
01GO基础-027异常处理2
01GO基础-028sync包详解
01GO基础-029Context
02GO进阶001包
02GO进阶002init()函数
02GO进阶003包的注意点
02GO进阶003使用go module导入本地包
02GO进阶004 time包
02GO进阶005 file操作
02GO进阶006 io操作
02GO进阶007 os包(文件 I/O、文件属性、目录与链接、创建和移除链接)
02GO进阶008复制文件
02GO进阶009断点续传
02GO进阶010 bufio包
02GO进阶011ioutil包
02GO进阶012遍历文件夹
02GO进阶013并发编程介绍
02GO进阶014Goroutine协程
02GO进阶015 GPM
02GO进阶016 runtime包
02GO进阶017 Channel
02GO进阶018 Goroutine池
02GO进阶019 定时器
02GO进阶020 select
02GO进阶021并发安全和锁
02GO进阶022sync
02GO进阶023原子操作
02GO进阶024 GMP原理与调度
02GO进阶025爬虫小案例
02GO进阶026 面向对象-匿名字段
02GO进阶026 面向对象-接口
02GO进阶027网络编程-互联网协议介绍
02GO进阶027网络编程-socket
02GO进阶027网络编程-http编程
02GO进阶027网络编程-websocket编程
02GO进阶028数据操作-MYSQL
02GO进阶028数据操作-REDIS
02GO进阶028数据操作-RTCD
02GO进阶028数据操作-ZOOKEEPER
02GO进阶028数据操作-KAFKA
02GO进阶028数据操作-RabbitMQ
02GO进阶028数据操作-ElasticSearch
02GO进阶028数据操作-NSQ
02GO进阶028数据操作-memcached
02GO进阶028数据操作-GORM
02GO进阶029beego框架-安装
02GO进阶029beego框架-快速入门
02GO进阶029beego框架-MVC架构介绍-controller设计-参数配置
02GO进阶029beego框架-MVC架构介绍-controller设计-路由设置
02GO进阶029beego框架-MVC架构介绍-controller设计-控制器函数
02GO进阶029beego框架-MVC架构介绍-controller设计-XSRF过滤
02GO进阶029beego框架-MVC架构介绍-controller设计-请求数据处理
02GO进阶029beego框架-MVC架构介绍-controller设计-Session控制
02GO进阶029beego框架-MVC架构介绍-controller设计-过滤器
02GO进阶029beego框架-MVC架构介绍-controller设计-Flash数据
02GO进阶029beego框架-MVC架构介绍-controller设计-URL构建
02GO进阶029beego框架-MVC架构介绍-controller设计-多种格式数据输出
02GO进阶029beego框架-MVC架构介绍-controller设计-表单数据验证
02GO进阶029beego框架-MVC架构介绍-controller设计-错误处理
02GO进阶029beego框架-MVC架构介绍-controller设计-日志处理
02GO进阶029beego框架-MVC架构介绍-model设计-概述
02GO进阶029beego框架-MVC架构介绍-model设计-CRUD操作
02GO进阶029beego框架-MVC架构介绍-model设计-高级查询
02GO进阶029beego框架-MVC架构介绍-model设计-原生SQL查询
02GO进阶029beego框架-MVC架构介绍-model设计-构造查询
02GO进阶029beego框架-MVC架构介绍-model设计-事务处理
02GO进阶029beego框架-MVC架构介绍-model设计-模型定义
02GO进阶029beego框架-MVC架构介绍-model设计-命令模式
02GO进阶029beego框架-MVC架构介绍-model设计-测试用例
02GO进阶029beego框架-MVC架构介绍-view设计-beego 模板语法指南
02GO进阶029beego框架-MVC架构介绍-view设计-模板处理
02GO进阶029beego框架-MVC架构介绍-view设计-其他
本文档使用 MrDoc 发布
-
+
首页
01GO基础-027异常处理2
## 基础知识 错误指的是可能出现问题的地方出现了问题,比如打开一个文件时失败,这种情况在人们的意料之中;而异常指的是不应该出现问题的地方出现了问题,比如引用了空指针,这种情况在人们的意料之外。可见,错误是业务过程的一部分,而异常不是 。 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 错误 和 异常 是可以互相转换的: 错误转异常,比如程序逻辑上尝试请求某个 URL,最多尝试三次,尝试三次的过程中请求失败是错误,尝试完第三次还不成功的话,失败就被提升为异常了。 异常转错误,比如 panic 触发的异常被 recover 恢复后,将返回值中 error 类型的变量进行赋值,以便上层函数继续走错误处理流程。 ## 一个启示 regexp 包中有两个函数 Compile 和 MustCompile,它们的声明如下: ```go func Compile(expr string) (*Regexp, error) func MustCompile(str string) *Regexp ``` 同样的功能,不同的设计: Compile 函数基于错误处理设计,将正则表达式编译成有效的可匹配格式,适用于用户输入场景。当用户输入的正则表达式不合法时,该函数会返回一个错误。 MustCompile 函数基于异常处理设计,适用于硬编码场景。当调用者明确知道输入不会引起函数错误时,要求调用者检查这个错误是不必要和累赘的。我们应该假设函数的输入一直合法,当调用者输入了不应该出现的输入时,就触发 panic 异常。 于是我们得到一个启示:什么情况下用错误表达,什么情况下用异常表达,就得有一套规则,否则很容易出现一切皆错误或一切皆异常的情况。 在这个启示下,我们给出 异常处理 的场景: 空指针引用 下标越界 除数为0 不应该出现的分支,比如 default 输入不应该引起函数错误 其他场景我们使用 错误处理,这使得我们的函数接口很精炼。对于异常,我们可以选择在一个合适的上游去 recover,并打印堆栈信息,使得部署后的程序不会终止。 说明: Golang 错误处理方式一直是很多人诟病的地方,有些人吐槽说一半的代码都是 "if err != nil { / 打印 && 错误处理 / }",严重影响正常的处理逻辑。当我们区分错误和异常,根据规则设计函数,就会大大提高可读性和可维护性。 ## 错误处理的正确姿势 ### 姿势一:失败的原因只有一个时,不使用 error 我们看一个案例: ```go 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,重构一下代码: ```go func (self *AgentContext) IsValidHostType(hostType string) bool { return hostType == "virtual_machine" || hostType == "bare_metal" } ``` 说明:大多数情况,导致失败的原因不止一种,尤其是对 I/O 操作而言,用户需要了解更多的错误信息,这时的返回值类型不再是简单的 bool,而是 error。 ### 姿势二:没有失败时,不使用 error error 在 Golang 中是如此的流行,以至于很多人设计函数时不管三七二十一都使用 error,即使没有一个失败原因。我们看一下示例代码: ```go func (self *CniParam) setTenantId() error { self.TenantId = self.PodNs return nil } ``` 对于上面的函数设计,就会有下面的调用代码: ```go err := self.setTenantId() if err != nil { // log // free resource return errors.New(...) } ``` 根据我们的正确姿势,重构一下代码: ```go func (self *CniParam) setTenantId() { self.TenantId = self.PodNs } ``` 于是调用代码变为: `self.setTenantId()` ### 姿势三:error 应放在返回值类型列表的最后 对于返回值类型 error,用来传递错误信息,在 Golang 中通常放在最后一个。 ```go resp, err := http.Get(url) if err != nil { return nill, err } ``` bool 作为返回值类型时也一样。 ```go value, ok := cache.Lookup(key) if !ok { // ...cache[key] does not exist… } ``` ### 姿势四:错误值统一定义,而不是跟着感觉走 很多人写代码时,到处 return errors.New(value),而错误 value 在表达同一个含义时也可能形式不同,比如“记录不存在”的错误 value 可能为: "record is not existed." "record is not exist!" "###record is not existed!!!" ... 这使得相同的错误 value 撒在一大片代码里,当上层函数要对特定错误 value 进行统一处理时,需要漫游所有下层代码,以保证错误 value 统一,不幸的是有时会有漏网之鱼,而且这种方式严重阻碍了错误 value 的重构。 于是,我们可以参考 C/C++ 的错误码定义文件,在 Golang 的每个包中增加一个错误对象定义文件,如下所示: ```go 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") ``` 说明:笔者对于常量更喜欢 C/C++ 的“全大写+下划线分割”的命名方式,读者可以根据团队的命名规范或个人喜好定制。 ### 姿势五:错误逐层传递时,层层都加日志 根据笔者经验,层层都加日志非常方便故障定位。 说明:至于通过测试来发现故障,而不是日志,目前很多团队还很难做到。如果你或你的团队能做到,那么请忽略这个姿势:) ### 姿势六:错误处理使用 defer 我们一般通过判断 error 的值来处理错误,如果当前操作失败,需要将本函数中已经 create 的资源 destroy 掉,示例代码如下: ```go 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 值。 根据这个结论,我们重构上面的示例代码: ```go 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 } ``` ### 姿势七:当尝试几次可以避免失败时,不要立即返回错误 如果错误的发生是偶然性的,或由不可预知的问题导致。一个明智的选择是重新尝试失败的操作,有时第二次或第三次尝试时会成功。在重试时,我们需要限制重试的时间间隔或重试的次数,防止无限制的重试。 两个案例: 我们平时上网时,尝试请求某个 URL,有时第一次没有响应,当我们再次刷新时,就有了惊喜。 团队的一个 QA 曾经建议当 Neutron 的 attach 操作失败时,最好尝试三次,这在当时的环境下验证果然是有效的。 ### 姿势八:当上层函数不关心错误时,建议不返回 error 对于一些资源清理相关的函数(destroy/delete/clear),如果子函数出错,打印日志即可,而无需将错误进一步反馈到上层函数,因为一般情况下,上层函数是不关心执行结果的,或者即使关心也无能为力,于是我们建议将相关函数设计为不返回 error。 ### 姿势九:当发生错误时,不忽略有用的返回值 通常,当函数返回 non-nil 的 error 时,其他的返回值是未定义的(undefined),这些未定义的返回值应该被忽略。然而,有少部分函数在发生错误时,仍然会返回一些有用的返回值。比如,当读取文件发生错误时,Read 函数会返回可以读取的字节数以及错误信息。对于这种情况,应该将读取到的字符串和错误信息一起打印出来。 说明:对函数的返回值要有清晰的说明,以便于其他人使用。 ## 异常处理的正确姿势 ### 姿势一:在程序开发阶段,坚持速错 去年学习 Erlang 的时候,建立了速错的理念,简单来讲就是“让它挂”,只有挂了你才会第一时间知道错误。在早期开发以及任何发布阶段之前,最简单的同时也可能是最好的方法是调用 panic 函数来中断程序的执行以强制发生错误,使得该错误不会被忽略,因而能够被尽快修复。 ### 姿势二:在程序部署后,应恢复异常避免程序终止 在 Golang 中,虽然有类似 Erlang 进程的 Goroutine,但需要强调的是 Erlang 的挂,只是 Erlang 进程的异常退出,不会导致整个 Erlang 节点退出,所以它挂的影响层面比较低,而 Goroutine 如果 panic 了,并且没有 recover,那么整个 Golang 进程(类似 Erlang 节点)就会异常退出。所以,一旦 Golang 程序部署后,在任何情况下发生的异常都不应该导致程序异常退出,我们在上层函数中加一个延迟执行的 recover 调用来达到这个目的,并且是否进行 recover 需要根据环境变量或配置文件来定,默认需要 recover。 这个姿势类似于 C 语言中的 断言,但还是有区别:一般在 Release 版本中,断言被定义为空而失效,但需要有 if 校验存在进行异常保护,尽管契约式设计中不建议这样做。在 Golang 中,recover 完全可以终止异常展开过程,省时省力。 我们在调用 recover 的延迟函数中以最合理的方式响应该异常: 打印堆栈的异常调用信息和关键的业务信息,以便这些问题保留可见; 将异常转换为错误,以便调用者让程序恢复到健康状态并继续安全运行。 我们看一个简单的例子: ```go func funcA() error { defer func() { if p := recover(); p != nil { fmt.Printf("panic recover! p: %v", p) debug.PrintStack() } }() return funcB() } func funcB() error { // simulation panic("foo") return errors.New("success") } func test() { err := funcA() if err == nil { fmt.Printf("err is nil\\n") } else { fmt.Printf("err is %v\\n", err) } } ``` 我们期望 test 函数的输出是: 1 err is foo 但实际上 test 函数的输出是: 1 err is nil 原因是 panic 异常处理机制不会自动将错误信息传递给 error,所以要在 funcA 函数中进行显式的传递,代码如下所示: ```go func funcA() (err error) { defer func() { if p := recover(); p != nil { fmt.Println("panic recover! p:", p) str, ok := p.(string) if ok { err = errors.New(str) } else { err = errors.New("panic") } debug.PrintStack() } }() return funcB() } ``` ### 姿势三:对于不应该出现的分支,使用异常处理 当某些不应该发生的场景发生时,我们就应该调用 panic 函数来触发异常。比如,当程序到达了某条逻辑上不可能到达的路径: ```go switch s := suit(drawCard()); s { case "Spades": // ... case "Hearts": // ... case "Diamonds": // ... case "Clubs": // ... default: panic(fmt.Sprintf("invalid suit %v", s)) } ``` ### 姿势四:针对入参不应该有问题的函数,使用 panic 设计 入参不应该有问题一般指的是硬编码,我们先看“一个启示”一节中提到的两个函数(Compile 和 MustCompile),其中 MustCompile 函数是对 Compile 函数的包装: ```go func MustCompile(str string) *Regexp { regexp, error := Compile(str) if error != nil { panic(`regexp: Compile(` + quote(str) + `): ` + error.Error()) } return regexp } ``` 所以,对于同时支持用户输入场景和硬编码场景的情况,一般支持硬编码场景的函数是对支持用户输入场景函数的包装。 对于只支持硬编码单一场景的情况,函数设计时直接使用 panic,即返回值类型列表中不会有 error,这使得函数的调用处理非常方便(没有了乏味的 “if err != nil {/ 打印 && 错误处理 /}” 代码块)。 ## 小结 本文以 Golang 为例,阐述了 错误 和 异常 的区别,并且分享了很多 错误 和 异常 处理的正确姿势,这些姿势可以单独使用,也可以组合使用,希望对大家有一点启发。
admin
2024年12月20日 16:23
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
分享
链接
类型
密码
更新密码