go 错误处理指北:error vs exception vs errno | go 技术论坛-金年会app官方网

很多有其他编程语言经验的人初次接触 go 语言时,想必对 if err != nil 的错误处理方式感到新奇,之后用久了,竟发现有点令人抓狂。

因为很多人不满 go 语言的错误处理方式,甚至有人做了一张梗图:

哈哈😄,不吹不黑,本文就来对比下 python、c 以及 go 这三种编程语言中的异常处理机制,看看你更喜欢哪一种。

python 错误处理

因为我接触的第一门编程语言是 python,所以我就先讲讲 python 中的错误处理机制。

python 的错误处理机制与 java、c#、javascript 等主流的高级编程语言非常类似,它们都可以算做是 exception 派系。

以下是 python 错误处理的典型示例程序:


def  div(a, b):
return a / b
try:
result = div(1, 0)
print(result)
except  zerodivisionerror  as e:
logging.error(e)
except  exception  as e:
logging.error(e)

div 函数内部不对参数进行任何校验,当除数 b0 时,代码会抛出 zerodivisionerror 错误。

我们在函数调用处使用 try...except... 语句对错误进行捕获并处理。捕获到 zerodivisionerror 表示遇到除数为 0 的情况,而捕获到 exception 则为了避免放过出现其他未知的异常。

从这两个错误的命名来看:zerodivisionerrorexception,一个表示「错误」,一个表示「异常」。其实它们都继承自 baseexception 基类,在 python 中并不区分错误和异常,所以 python 中的错误处理,我们一般称为异常处理。所有 exception 派系的编程语言也都类似。

除了内置异常,我们也可以很方便的定义自己的异常类:


class  myexception(exception):
...

没错,就是这么简单。

可以按照如下方式使用自定义异常:


raise myexception("my custom exception")

这与内置异常没什么两样,try...except... 同样能够正常捕获自定义异常。

其实这种 try...except... 方式处理异常,最大的好处就是:异常兜底

在一整个代码块中间,无论写了多少行代码,无论哪里可能出现异常,我们都可以放心大胆的不去处理异常,而是在代码块最外层使用 try...except... 进行捕获,示例如下:


def  a():
...
def  b():
...
def  c():
...
def  main():
try:
a()
b()
c()
except  exception  as e:
logging.error(e)
else:
print("success")
finally:
print("release resources")

这种方式极大的简化了我们的代码编写,不用在每一个函数调用处都对异常进行处理,仅需要在最外层做一次异常处理即可。

当然,这种方式也有弊端,那就是异常不再是异常。什么意思?就是说,在 python 中的异常其实是不分轻重的,有些异常可以忽略,有些异常又不可以忽略,但上面的示例代码显然无法区分异常的严重程度。

并且,因为有异常兜底,很多开发者写代码的时候就会更加随性,写出来的程序就会更加不可控。

接下来,我们再看一看 c 语言中的错误处理。

c 错误处理

虽然我在工作中没怎么写过 c,但 c 乃“万物鼻祖”。既然要对比多种编程语言错误处理机制,c 自然必不可少。

其实 c 语言并没有提供对错误处理的直接支持。一般来说,在 c 语言内部函数代码出现错误时,会返回 1null,并且同时会设置一个错误码 errno,以此来表示错误类型。

相比于其他高级语言对错误的抽象,c 语言的错误处理就显得比较“原始”了。

以下是 c 语言错误处理的典型示例程序:


#include  
#include  
#include    // 包含 strerror 函数的头文件
int  main() {
// 清除 errno 初始值,这是一个好的编程习惯
errno = 0;
file *file;
char *filename = "example.txt";
// 尝试以读取模式打开文件
file = fopen(filename, "r");
if (file == null) {
// 打开文件出错
printf("failed to open file: %s\n", filename);
// 查看错误码 errno
printf("errno: %d\n", errno);
// perror 函数显示传入的字符串,后跟一个冒号、一个空格和当前 errno 值的文本表示形式
perror("error");
// strerror 函数返回一个指针,指向当前 errno 值的文本表示形式
printf("error: %s\n", strerror(errno));
// 在发生错误时,大多数的 c 或 unix 函数调用返回 1 或 null
return  1;
}
// 文件操作
printf("open file success\n");
// process file...
// 完成操作后,关闭文件
fclose(file);
return  0;
}

我们尝试使用 fopen 来打开一个文件,fopen 函数返回值 file 可能是 file* 表示文件描述符,也可能是 null 表示函数出错。

所以我们需要通过 if (file == null) 来判断打开文件是否出错,如果出错,全局变量 errno 会被赋值,使用 perror 函数或者 strerror(errno) 可以读取错误码对应的错误描述信息。

note:

perror() 函数打印传入的字符串,后跟一个冒号、一个空格和当前 errno 值的文本表示形式。

strerror() 函数,返回一个指针,指针指向当前 errno 值的文本表示形式。

note:

我们能在 头文件中找到错误码定义:

/*

  • error codes

*/

#define eperm 1 /* operation not permitted */

#define enoent 2 /* no such file or directory */

#define esrch 3 /* no such process */

#define eintr 4 /* interrupted system call */

#define eio 5 /* input/output error */

#define enxio 6 /* device not configured */

#define e2big 7 /* argument list too long */

执行示例代码,打开一个不存在的文件,输出如下:


$  gcc  main.c  -g  -o  main
$  ./main
failed  to  open  file:  example.txt
errno:  2
error:  no  such  file  or  directory
error:  no  such  file  or  directory

这种错误处理方式存在一个很大的问题:返回值有二义性

fopen 函数即可能返回 file* 也可能返回 null,这是一种很糟糕的做法。

并且,很多人会因此忘记错误检查。

而在 c 语言中,之所以要这样处理错误,主要是因为 c 语言的函数只能有一个返回值。所以这也是一种没有办法的办法。

不过,这种方式并不能处理所有错误场景。

如果用 c 来实现 div 函数,就不能使用 null 作为返回值,null 无法转换成 int 类型。

我们只能这样做:


#include  
int  div(int  a, int  b, int *result) {
if (b == 0) {
return -1; // 返回 -1 表示错误
}
*result = a / b; // 将结果存储在指针所指向的变量中
return  0; // 返回 0 表示成功
}
int  main() {
int result;
int err;
err = div(1, 0, &result);
// 错误处理
if (err == -1) {
printf("division by zero\n");
} else {
printf("result: %d\n", result);
}
return  0;
}

div 函数还是单返回值,不过这个返回值只代表错误,返回 0 表示成功,返回 -1 表示失败。

div 函数计算结果通过参数的形式,被存储在指针所指向的变量 int *result 中。

这也是 c 语言中错误处理的另一种典型做法。

但这种用参数接收计算结果的方式,我个人其实是不太喜欢的,因为我认为语义不够清晰。

note:

其实 c 函数支持返回结构体指针,这样虽然函数只能返回一个值,但可玩性就大大增加了,所以单返回值也不是不能接受。

最后,我们再看一看 c 语言中的错误处理。

go 错误处理

我在前文说 c 是“万物鼻祖”,这在编程世界里并不算太夸张的说法。python 解释器就是用 c 语言写的,而 go 语言作者之一 同时又是 unix 操作系统和 b 语言(c 语言的前身)作者,所以 go 在设计之初,就大量参考了 c 语言的设计。

不难推测,go 的错误处理也参考了 c 语言。事实也的确如此,go 的错误处理继承自 c 语言,并在其基础上做了改进。

以下是 go 语言错误处理的典型示例程序:


package  main
import (
"errors"
"fmt"
"log"
)
func  div(a, b  int) (int, error) {
if  b == 0 {
return  0, errors.new("division by zero")
}
return  a / b, nil
}
func  main() {
result, err := div(1, 0)
if  err != nil {
fmt.println(err)
return
}
fmt.println(result)
}

得益于 go 函数支持多返回值,所以 go 就不再需要 errno 了。

go 定义了一个 error 类型,专门用来表示错误,使用 errors.new 可以轻松构建一个错误对象。

这也就引出了经典的 if err != nil,程序遇到错误就会走入此分支,我们需要在此做错误处理。

go 中的 error 其实是一个接口:


type  error  interface {
error() string
}

任何实现了 error 接口的类型,都可以是一个错误。

而我们通过 errors.new 创建的错误,其实就是一个内置错误类型的实现:


func  new(text  string) error {
return &errorstring{text}
}
type  errorstring  struct {
s  string
}
func (e *errorstring) error() string {
return  e.s
}

我们也完全可以根据自己的需要,自定义 error


type  myerror  struct {
msg  string
}
func (e *myerror) error() string {
return  e.msg
}

并且 myerror 中可以增加任何我们想要的字段和方法。

我们可以像这样处理不同类型的错误:


if  err != nil {
switch  err.(type) {
case *myerror:
// ...
case *zerodivisionerror:
// ...
default:
// ...
}
}

可以发现,go 中的错误处理其实是对返回值的检查,并且我们可以通过类型断言 err.(type) 来判断 error 的具体类型。

有时候,我们想忽略错误,需要这样做:


result, _ := div(1, 0)

在 go 中必须使用 _ 显式的忽略错误,否则如果不处理 err 程序会编译不通过。

因为 go 函数可以返回多个值,所以这给我们处理错误带来了更大的灵活性,比如下面这个示例:


type  smscode  struct{}
func (c smscode) verify1() (bool, error) {
// ...
}
func (c smscode) verify2() error {
// ...
}

假如我们有一个发送短信验证码的结构体 smscode,需要为其定义 verify 方法来验证用户输入的短信验证码是否正确。

我们可以定义成 verify1 这样,方法返回两个值,bool 值表示验证码是否正确,error 值表示代码执行过程中是否出错,比如 redis 无法调通等。

我们也可以定义成 verify2 这样,方法仅返回一个值,验证码错误也可以当作是一种 error 类型,比如 errors.new("invalid sms code")

当然,这就是业务上需要我们自己做决策的地方了,和语言本身错误处理无关。这也正是因为 go 提供的方便,让我们比在写 c 代码时可以更加灵活的处理错误。

不过,go 中的错误处理也有弊端,看下面这个示例你就有体会了:


func  a() error {
// ...
}
func  b() error {
// ...
}
func  c() error {
// ...
}
func  main() {
err := a()
if  err != nil {
log.fatal(err)
}
err = b()
if  err != nil {
log.fatal(err)
}
err = c()
if  err != nil {
log.fatal(err)
}
}

是不是有点抓狂。

哈哈,其实只要我们做好封装,还是可以避免这种代码产生的。

相较于 python,go 对错误处理更加谨慎,我们不能通过在代码块最外层写一个 try...except... 来进行兜底,只能一步一步去处理错误。

此外,python 在异常处理中提供了 finally 语句可以方便我们释放资源,不论代码是否执行出错,finally 语句最终都会执行。

go 也提供了类似功能,即 defer 语句:


func  main() {
defer  func() {
fmt.println("release resources")
}()
result, err := div(1, 0)
if  err != nil {
fmt.println(err)
return
}
fmt.println(result)
}

执行示例代码:


$  go  run  main.go
division  by  zero
release  resources

defer 修饰的函数会在外层的 main 函数退出前被调用执行。

note:

c 语言可以使用 跳转到指定代码块来进行资源释放。

另外,go 与其他主流编程语言在错误处理上有一个很大的分歧,go 区分了「错误」和「异常」。


package  main
import  "fmt"
func  maypanic() {
panic("a problem")
}
func  main() {
defer  func() {
if  r := recover(); r != nil {
fmt.println("recovered error:", r)
} else {
fmt.println("recovered success")
}
}()
maypanic()
fmt.println("after maypanic()")
}

在 go 中 panic 表示一个异常,与 error 错误不同,默认情况下,遇到 panic 调用,go 程序会崩溃并退出。

可以使用 recover 来捕获 panic 抛出的异常,但是要注意 recover 一定要放在函数入口处的 defer 语句中,因为只有这样才能保证 recover 会被调用。

我们可以在主动捕获异常的地方,自行处理出现异常后的逻辑。

执行示例代码:


$  go  run  main.go
recovered  error:  a  problem

可以发现 after maypanic() 并没有被打印。这说明代码执行到 panic 以后就中断了,panic 会抛出异常,然后异常被 recover 捕获并处理。

recover 可以看作是一种全局的兜底异常处理方案,gin 框架就通过中间件的方式,在处理请求的时候 recover 住未知的异常,以防程序退出。

不过绝大多数情况下并不建议使用 panic recover 机制,panic 表示意外的异常,而我们在编写代码阶段,更多需要处理的情况是可以预见的错误 error

对于真正意外的情况,比如数组索引越界,我们才会使用 panic,对于一般性错误,我们应该是使用 error 来进行处理。

以上便是 go 语言的错误处理机制。

总结

python 的错误处理方式在主流编程语言中最为常见,只不过其他编程语言的关键字一般为 try...catch...finally

这种异常处理方式的好处是代码简洁优雅,所以是最被程序员所接受的一种错误处理方式。

不知道你有没有注意,我在 python 异常处理的示例代码中,还写了一个 else 分支,其实 python 异常处理完整逻辑是 try...except...else...finallyelse 分支的功能你可以自己思考下是干什么用的。

c 的错误处理比较“原始”,很大原因是 c 函数只能返回单个值。所以就可能出现一个返回值,存在多种用途的现象。不过既然都用 c 语言开发程序了,这一点显然是要程序员自己要解决的问题。

go 因为是后起之秀,所以可以参考它的前辈们,来设计属于自己风格的错误处理机制。不过即使这样,go 的错误处理仍然是被吐槽最多的。

我倒是觉得这种错误处理非常的 “go”,很有 go 语言的特点,大道至简。

对比了三种主流编程语言的错误处理以后,你是否对 go 语言的错误处理有了更新的认识?你喜欢哪种错误处理方式?可以在评论区进行交流。

本文示例源码我都放在了 中,欢迎点击查看。

希望此文能对你有所启发。

本文 github 示例代码:

联系我

本作品采用《cc 协议》,转载必须注明作者和本文链接
野生程序员|公众号:go编程世界
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
网站地图