3 4 异常处理

3.4 异常处理 #

本节源码位置 https://github.com/golang-minibear2333/golang/blob/master/3.grammar-advancement/3.4-errors

3.4.1 异常处理思想 #

go 语言里是没有 try catch 的概念的,因为 try catch 会消耗更多资源,而且不管从 try 里面哪个地方跳出来,都是对代码正常结构的一种破坏。

所以 go 语言的设计思想中主张

  • 如果一个函数可能出现异常,那么应该把异常作为返回值,没有异常就返回 nil
  • 每次调用可能出现异常的函数时,都应该主动进行检查,并做出反应,这种 if 语句术语叫卫述语句

所以异常应该总是掌握在我们的手上,保证每次操作产生的影响达到最小,保证程序即使部分地方出现问题,也不会影响整个程序的运行,及时的处理异常,这样就可以减轻上层处理异常的压力。

同时也不要让未知的异常使你的程序崩溃。

3.4.2 异常的形式 #

我们应该让异常以这样的形式出现

func Demo() (int, error)

我们应该让异常以这样的形式处理(卫述语句)

_,err := errorDemo()
	if err!=nil{
		fmt.Println(err)
		return
	}

3.4.3 自定义异常 #

比如程序有一个功能为除法的函数,除数不能为 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 异常", "喝太多")

3.4.4 详细的异常信息 #

上面的异常信息只是简单的返回了一个字符串而已,想在报错的时候保留现场,得到更多的异常内容怎么办呢?这就要看看 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", "/home/how_to_code/README")
fmt.Println(f.Error())

输出

路径为 /home/how_to_code/README 的文件 README  操作时出错

3.4.5 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 语句。

3.4.6 panic #

刚刚有说到 defer 是崩溃后,仍然会被调用的语句,那程序在什么情况下会崩溃呢?

Go 的类型系统会在编译时捕获很多异常,但有些异常只能在运行时检查,如数组访问越界、空指针引用等。这些运行时异常会引起 painc 异常(程序直接崩溃退出)。然后在退出的时候调用当前 goroutinedefer 延迟调用语句。

有时候在程序运行缺乏必要的资源的时候应该手动触发宕机(比如配置文件解析出错、依赖某种独有库但该操作系统没有的时候)

defer fmt.Println("关闭文件句柄")

panic("人工创建的运行时异常")

报错如下

报错示例

3.4.7 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 可以放在最外层函数,做统一异常处理。

3.4.8 小结 #

deferpanic是面试的高频题,因为在工作中非常常用。

自定义错误的语法在正规项目中最为常用,可以和我一起实战一定能体验到了。



本图书由小熊©2021 版权所有,所有文章采用知识署名-非商业性使用-禁止演绎 4.0 国际进行许可。