[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` 异常处理,我所能想到和找到的全部内容了,希望你在工作中用的更顺手。
- 安装与下载
- 投稿大纲
- 第一章、跑起来
- 为什么要学 go 语言?
- 让你的 Golang 项目在 IDE 里跑起来
- 第一个 go 程序
- go mod最佳实践
- 第二章、语言基础
- 声明【变量】的各种方式
- 常量+各种类型转换
- switch和type switch
- 循环语句的多种形式、死循环、break/continue
- range深度解析
- 第三章、函数和容器
- 函数简单使用和基本知识解析
- 匿名函数和闭包
- 可变参数
- 集合(map)
- 数组和切片
- 切片知识补充
- 第四章、指针、结构体、接口和异常处理
- 指针讨论
- 结构体
- 接口与多态
- 异常处理
- 第五章、再学一点运用自如
- 其他常用操作
- Go文件操作大全
- Go代码基本标准规范
- 切片排序sort包的使用
- Golang打镜像Dockerfile的写法
- 等待goroutine完成任务,循环中使用goroutine
- kinping.flag包读取命令行配置