小熊今天有意外收获,忍不住给大家分享我愉快的心情!昨天中午下楼取外卖的时候被一个同事认出来了,他问我:“是不是【编程三分钟】的作者,文章写的不错”。
你知道吗!我当时就是一愣,然后差点感动到哭出来,虽然小熊的号比不上大牛的号,不能随便发一篇文章都有成千上万的阅读量;但是非常开心的是,我还有你们,默默的关注我,爱你们~!
今天想和大家聊聊 golang 的异常处理
异常处理思想
在 go
语言里是没有 try catch
的概念的,因为 try catch
会消耗更多资源,而且不管从 try
里面哪个地方跳出来,都是对代码正常结构的一种破坏。
所以 go
语言的设计思想中主张
如果一个函数可能出现异常,那么应该把异常作为返回值,没有异常就返回 nil
每次调用可能出现异常的函数时,都应该主动进行检查,并做出反应,这种 if 语句术语叫卫述语句
所以异常应该总是掌握在我们的手上,保证每次操作产生的影响达到最小,保证程序即使部分地方出现问题,也不会影响整个程序的运行,及时的处理异常,这样就可以减轻上层处理异常的压力。
同时也不要让未知的异常使你的程序崩溃。
异常的形式
我们应该让异常以这样的形式出现
func Demo() (int, error)
我们应该让异常以这样的形式处理(卫述语句)
_,err := errorDemo() if err!=nil{ fmt.Println(err) return }
自定义异常
比如程序有一个功能为除法的函数,除数不能为 0
,否则程序为出现异常,我们就要提前判断除数,如果为 0
返回一个异常。那他应该这么写。
func divisionInt(a, b int) (int, error) { if b == 0 { return -1, errors.New("除数不能为0") } return a / b, nil }
这个函数应该被这么调用
a, b := 4, 0 res, err := divisionInt(a, b) if err != nil { fmt.Println(err.Error()) return } fmt.Println(a, "除以", b, "的结果是 ", res)
可以注意到上面的两个知识点
- 创建一个异常
errors.New("字符串")
- 打印异常信息
err.Error()
只要记得这些,你就掌握了自定义异常的基本方法。
但是 errors.New("字符串")
的形式我不建议使用,因为他不支持字符串格式化功能,所以我一般使用 fmt.Errorf
来做这样的事情。
err = fmt.Errorf("产生了一个 %v 异常", "喝太多")
详细的异常信息
上面的异常信息只是简单的返回了一个字符串而已,想在报错的时候保留现场,得到更多的异常内容怎么办呢?这就要看看 errors
的内部实现了。其实相当简单。
errors
实现了一个叫 error
的接口,这个接口里就一个 Error
方法且返回一个 string
,如下
type error interface { Error() string }
只要结构体实现了这个方法就行,源码的实现方式如下
type errorString struct { s string } func (e *errorString) Error() string { return e.s } // 多一个函数当作构造函数 func New(text string) error { return &errorString{text} }
所以我们只要扩充下自定义 error 的结构体字段就行了。
这个自定义异常可以在报错的时候存储一些信息,供外部程序使用
type FileError struct { Op string Name string Path string } // 初始化函数 func NewFileError(op string, name string, path string) *FileError { return &FileError{Op: op, Name: name, Path: path} } // 实现接口 func (f *FileError) Error() string { return fmt.Sprintf("路径为 %v 的文件 %v,在 %v 操作时出错", f.Path, f.Name, f.Op) }
调用
f := NewFileError("读", "README.md", "/home/how_to_code/README.md") fmt.Println(f.Error())
输出
路径为 /home/how_to_code/README.md 的文件 README.md,在 读 操作时出错
defer
上面说的内容很简单,在工作里也是最常用的,下面说一些拓展知识。
Go 中有一种延迟调用语句叫 defer 语句,它在函数返回时才会被调用,如果有多个 defer 语句那么它会被逆序执行。
比如下面的例子是在一个函数内的三条语句,他是这么怎么执行的呢?
defer fmt.Println("see you next time!") defer fmt.Println("close all connect") fmt.Println("hei boy")
输出如下, 可以看到两个 defer
在程序的最后才执行,而且是逆序。
hei boy close all connect see you next time!
这一节叫异常处理详解,终归是围绕异常处理来讲述知识点, defer 延迟调用语句的用处是在程序执行结束,甚至是崩溃后,仍然会被调用的语句,通常会用来执行一些告别操作,比如关闭连接,释放资源(类似于 c++ 中的析构函数)等操作。
涉及到 defer
的操作
- 并发时释放共享资源锁
- 延迟释放文件句柄
- 延迟关闭
tcp
连接 - 延迟关闭数据库连接
这些操作也是非常容易被人忘记的操作,为了保证不会忘记,建议在函数的一开始就放置 defer
语句。
panic
刚刚有说到 defer 是崩溃后,仍然会被调用的语句,那程序在什么情况下会崩溃呢?
Go 的类型系统会在编译时捕获很多异常,但有些异常只能在运行时检查,如数组访问越界、空指针引用等。这些运行时异常会引起 painc 异常(程序直接崩溃退出)。然后在退出的时候调用当前 goroutine 的 defer 延迟调用语句。
有时候在程序运行缺乏必要的资源的时候应该手动触发宕机(比如配置文件解析出错、依赖某种独有库但该操作系统没有的时候)
defer fmt.Println("关闭文件句柄") panic("人工创建的运行时异常")
报错如下
panic recover
出现 panic 以后程序会终止运行,所以我们应该在测试阶段发现这些问题,然后进行规避,但是如果在程序中产生不可预料的异常(比如在线的web或者rpc服务一般框架层),即使出现问题(一般是遇到不可预料的异常数据)也不应该直接崩溃,应该打印异常日志,关闭资源,跳过异常数据部分,然后继续运行下去,不然线上容易出现大面积血崩。
然后再借助运维监控系统对日志的监控,发送告警给运维、开发人员,进行紧急修复。
语法如下:
func divisionIntRecover(a, b int) (ret int) { defer func() { if err := recover(); err != nil { // 打印异常,关闭资源,退出此函数 fmt.Println(err) ret = -1 } }() return a / b }
调用
var res int datas := []struct { a int b int }{ {2, 0}, {2, 2}, } for _, v := range datas { if res = divisionIntRecover(v.a, v.b); res == -1 { continue } fmt.Println(v.a, "/", v.b, "计算结果为:", res) }
输出结果
runtime error: integer divide by zero 2 / 2 计算结果为: 1
调用 panic 后,当前函数从调用点直接退出
recover 函数只有在 defer 代码块中才会有效果
recover 可以放在最外层函数,做统一异常处理。
这就是 go 异常处理,我所能想到和找到的全部内容了,希望你在工作中用的更顺手。
小熊虽然工作忙,文章没办法发的那么频繁,但是我有时间就写一点,反复校对,代码也反复测试最后放 github 上,这样文章的内容会更完整、更有逻辑、更少异常、对读者对自己都更负责。如果你发现了文章中出现问题,欢迎在评论区和我讨论,非常感谢!