错误处理
错误处理是学习编程语言都需要考虑的一个重要话题。在早期的语言中,错误处理不是语言规范的一部分,通常只作为一种编程范式存在。但自C++语言以来,语言层面上会增加错误处理的支持,比如异常(Exception)的概念和try-catch关键字的引入。java中的Error表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java虚拟机)出现的问题。例如,当JVM不再有继续执行操作所需的内存资源时,将出现OutOfMemoryError内存溢出错误,Go语言在此功能上考虑得更为深远。漂亮的错误处理规范是Go语言的亮点之一。
error接口
Go语言引入关于错误处理的标准模式,error接口,接口定义如下:
type error interface {
Error() string
}
对于多数函数,如果要返回错误,大致上都可以将error作为多重返回值中的最后一个,但这并非是强制要求。
//error当做函数做后一个值返回
func Foo(param int)(n int, err error) {
// ...
}
当调用代码的时候可以这样处理错误流程
n, err := Foo(0)
if err != nil {
// 错误处理
} else {
// 使用返回值n
}
首先,定义一个用于承载错误信息的类型。因为Go语言中接口的灵活性,你根本不需要从error接口继承或者像Java一样需要使用implements来明确指定该类型和接口之间的关系,如下:
type PathError struct {
Op string
Path string
Err error
}
要让编译器知道PathError可以当做一个error来传递,关键在于代码的实现,如下:
func (e *PathError) Error() string {
return e.Op + " " + e.Path + ": " + e.Err.Error()
}
再比如将error包装成一个PathError对象返回
func Stat(name string) (fi FileInfo, err error) {
var stat syscall.Stat_t
err = syscall.Stat(name, &stat)
if err != nil {
//保装成PathError对象,我可以简单理解返回PathError对象为错误的code,错误的名称,错误的具体信息
return nil, &PathError{"stat", name, err}
}
return fileInfoFromStat(&stat, name), nil
}
defer
关键字defer是Go语言引入的一个关闭资源很灵活使用的关键字,个人觉得是为了退出程序更好的关闭使用的资源或者连接,下面是一段c++代码:
class file_closer {
FILE _f;
public:
file_closer(FILE f) : _f(f) {}
~file_closer() { if (f) fclose(f); }
};
然后再调用的地方使用:
void f() {
FILE f = open_file("file.txt"); // 打开一个文件句柄
file_closer _closer(f);
// 对f句柄进行操作
}
file_closer这个包装类在每一个return的位置去关掉之前打开的文件句柄,因为如果没有这个类,代码中所有退出函数的环节,都将因为没有关闭句柄而出现异常。好比你进门打扫完卫生不管以哪种方式离开房间都需要关好使用的电灯,水龙头,门窗一样,即使你头脑清晰,想明白了每一个分支和可能出错的条件,在该关闭的地方都关闭了,怎么保证你的后继者也能做到同样关闭?大量莫名其妙的问题就出现了。
在C/C++中还有另一种解决方案。开发者可以将需要释放的资源变量都声明在函数的开头部分,并在函数的末尾部分统一释放资源。函数需要退出时,就必须使用goto语句跳转到指定位置先完成资源清理工作,而不能调用return语句直接返回。就是在离开前都去走一遍检查门窗关门流程。而Go语言使用defer关键字简简单单地解决了这个问题。如下:
func CopyFile(dst, src string) (w int64, err error) {
srcFile, err := os.Open(src)
if err != nil {
return
}
defer srcFile.Close()
dstFile, err := os.Create(dstName)
if err != nil {
return
}
defer dstFile.Close()
return io.Copy(dstFile, srcFile)
}
上面的代码如果没有defer srcFile.Close()
,在执行os.Create(dstName)
的时候出现了错误,程序退出了,但之前创建的src(文件句柄)没有被释放。加上defer,我们可以在代码中优雅的关闭/清理代码中所使用的变量:src(文件句柄)。关于defer关键字注意以下三点:
- 当defer被声明时,其参数就会被实时解析 就是在声明defer的时候声明的参数值是多少就是多少,之后的变量操作不会起作用。通过运行结果,可以看到defer输出的值,就是定义时的值。而不是defer真正执行时的变量值。如下:
func put() {
i := 0
defer fmt.Println(i) //输出0,因为i此时就是0,在这一行声明defer的时候i:=0
i++
defer fmt.Println(i) //输出1,因为i此时就是1,在这一行声明defer的时候i++了变成1
return
}
- defer执行顺序为先进后出 当同时定义了多个defer代码块时,Go按照先定义后执行的顺序依次调用defer。没有为什么就是这样规定的我也不知道为什么!!!
func a() {
for i := 0; i < 5; i++ {
defer fmt.Print(i)
}
}
输出结果依次为:4,3,2,1,0结果是倒着执行的。
- defer可以读取有名返回值
func a() (i int) {
defer func() { i-- }()
return 1
}
defer是在return调用之后才执行的。这里需要明确的是defer代码块的作用域仍然在函数之内,defer的作用域仍然在a()函数之内。因此defer仍然可以读取a()函数内的变量(如果无法读取函数内变量,那么defer就无法关闭文件句柄了嘛!!!)当执行return 1 之后,i的值还是1,此时此刻,defer代码块开始执行,对i进行自减操作,因此输出1-1=0,结果为0。
打个记号
加班回来已经快凌晨十二点了,为了薅掘金的大茶缸,现在已经是1点24了,我之前咋就没这么认真过!!!!好了滚蛋了!!!
备注
本文正在参与「掘金Golang主题学习月」, 点击查看活动详情。