企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
[TOC] 今天想和大家聊聊 `golang` 的异常处理 > go语言github项目:https://github.com/minibear2333/how_to_code ### 异常处理思想 在 `go` 语言里是没有 `try catch` 的概念的,因为 `try catch` 会消耗更多资源,而且不管从 `try` 里面哪个地方跳出来,都是对代码正常结构的一种破坏。 所以 `go` 语言的设计思想中主张 * 如果一个函数可能出现异常,那么应该把异常作为返回值,没有异常就返回 `nil` * 每次调用可能出现异常的函数时,都应该主动进行检查,并做出反应,这种 `if` 语句术语叫**卫述语句** 所以异常应该总是掌握在我们的手上,保证每次操作产生的影响达到最小,保证程序即使部分地方出现问题,也不会影响整个程序的运行,及时的处理异常,这样就可以减轻上层处理异常的压力。 同时也不要让未知的异常使你的程序崩溃。 ### 异常的形式 我们应该让异常以这样的形式出现 ```Go func Demo() (int, error) ``` 我们应该让异常以这样的形式处理(卫述语句) ```Go _,err := errorDemo() if err!=nil{ fmt.Println(err) return } ``` ### 自定义异常 比如程序有一个功能为除法的函数,除数不能为 `0` ,否则程序为出现异常,我们就要提前判断除数,如果为 `0` 返回一个异常。那他应该这么写。 ```Go func divisionInt(a, b int) (int, error) { if b == 0 { return -1, errors.New("除数不能为0") } return a / b, nil } ``` 这个函数应该被这么调用 ```Go 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` 来做这样的事情。 ```Go err = fmt.Errorf("产生了一个 %v 异常", "喝太多") ``` ### 详细的异常信息 上面的异常信息只是简单的返回了一个字符串而已,想在报错的时候保留现场,得到更多的异常内容怎么办呢?这就要看看 `errors` 的内部实现了。其实相当简单。 `errors` 实现了一个叫 `error` 的接口,这个接口里就一个 `Error` 方法且返回一个 `string` ,如下 ```Go type error interface { Error() string } ``` 只要结构体实现了这个方法就行,源码的实现方式如下 ```Go type errorString struct { s string } func (e *errorString) Error() string { return e.s } // 多一个函数当作构造函数 func New(text string) error { return &errorString{text} } ``` 所以我们只要扩充下自定义 `error` 的结构体字段就行了。 这个自定义异常可以在报错的时候存储一些信息,供外部程序使用 ```Go 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) } ``` 调用 ```Go f := NewFileError("读", "README.md", "/home/how_to_code/README.md") fmt.Println(f.Error()) ``` 输出 ```Go 路径为 /home/how_to_code/README.md 的文件 README.md,在 读 操作时出错 ``` ### defer 上面说的内容很简单,在工作里也是最常用的,下面说一些拓展知识。 `Go` 中有一种延迟调用语句叫 `defer` 语句,它在函数返回时才会被调用,如果有多个 `defer` 语句那么它会被逆序执行。 比如下面的例子是在一个函数内的三条语句,他是这么怎么执行的呢? ```Go 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` 延迟调用语句。 有时候在程序运行缺乏必要的资源的时候应该手动触发宕机(比如配置文件解析出错、依赖某种独有库但该操作系统没有的时候) ```Go defer fmt.Println("关闭文件句柄") panic("人工创建的运行时异常") ``` 报错如下 ![报错示例](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9jb2RpbmczbWluLm9zcy1hY2NlbGVyYXRlLmFsaXl1bmNzLmNvbS8yMDIwLzA1LzI4L0JsNHl1eDIwNTEucG5n?x-oss-process=image/format,png) ### panic recover 出现 `panic` 以后程序会终止运行,所以我们应该在测试阶段发现这些问题,然后进行规避,但是如果在程序中产生不可预料的异常(比如在线的web或者rpc服务一般框架层),即使出现问题(一般是遇到不可预料的异常数据)也不应该直接崩溃,应该打印异常日志,关闭资源,跳过异常数据部分,然后继续运行下去,不然线上容易出现大面积血崩。 然后再借助运维监控系统对日志的监控,发送告警给运维、开发人员,进行紧急修复。 语法如下: ```Go func divisionIntRecover(a, b int) (ret int) { defer func() { if err := recover(); err != nil { // 打印异常,关闭资源,退出此函数 fmt.Println(err) ret = -1 } }() return a / b } ``` 调用 ```Go 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` 异常处理,我所能想到和找到的全部内容了,希望你在工作中用的更顺手。