在日常编写golang程序或阅读别人的golang代码时,我们总会看到如下的一堆代码块:
xx, err = func(xx)
if err != nil {
//do sth. to tackle this problem
}
这种经典的显式错误处理方式,在golang开发中几乎无处不在,了解过golang开发的同学肯定会很熟悉、但同时又可能很痛恨这种麻烦的错误处理方式。
那么golang语言开发者为什么要这样设计呢?nil的意义是什么?它又有什么有趣的应用?对于这些问题,笔记会在下文逐一介绍。
错误:意料之内,可能出的问题, 比如网络连接超时,json解析错误等
异常:意料之外,不应该出的问题,比如空指针异常,下标溢出等异常
由上可知,错误与异常的区别,主要在于error是否在预料之内。而能否很好地操控与处理预料之内的error,往往能看出一个程序员水平的高低。
golang有两种错误处理方式,分别是错误返回和捕获异常。
golang 使用error接口作为标准错误接口,在标准库函数中,error通常作为函数的最后一个返回值:
func func()(xx, err error){
//code
return xx, err
}
为了处理预料之内的error,golang强制要求程序员处理错误返回,即我们常见的代码片段:
xx, err = func(xx)
if err != nil {
//do sth. to tackle this problem
}
当函数执行正确时,返回的err=nil;若err!=nil,程序员需要编写错误提示等。
其中nil的含义及作用可见【三、补充】。
为了处理预料之外的error,golang使用用到了panic、recover两个内置函数和一个关键字defer来处理异常。
panic——用于抛出异常
recover——捕获异常
defer——声明延迟函数
golang的异常处理/异常捕获过程,可以简单地概括为:
在defer声明的延迟函数中,通过recover捕获panic抛出的异常。
(其中defer要在panic之前进行声明)
例子:使用defer + recover来捕获和处理异常
package main
import (
"fmt"
"time"
)
func test(){
//使用defer + recover来捕获和处理异常
defer func(){
err := recover() // recover()是go内置函数,可以捕获到异常
if err != nil { //err 不为空
fmt.Println("err=", err)
}
}()//匿名函数
num1 := 18342026
num2 := 0
res := num1/num2 //异常error
fmt.Println("res=", res)
}
func main(){
test()
fmt.Println("func_test done.")
}
在许多网络论坛中,不少人吐槽golang的“err != nil”的错误处理方式不够优雅,直言在调用各种库函数时都得添加这一代码片段既显得“啰嗦”,又会影响自己的项目开发效率。
然而在笔者看来,这却是一种优秀的安全机制,它强制要求程序员处理有可能产生的error,对于维护项目的安全性与健壮性而言十分重要。
而且,对于熟悉c/c++编程、又觉得java编程比较臃肿的笔者来说,golang的“err != nil”可比java的try catch优雅多了~
此外,对于不喜欢或想要忽略已知的错误时,也有其他的办法避免“err != nil”的操作,具体可见【三、补充】。
nil和null类似,都是表示空/零。在Go语言中,布尔类型的"0"(初始值)为false,数值类型的"0"为0,字符串类型的"0"为空字符串"",而指针/切片/映射/通道/函数和接口的"0"即为nil。
虽然预先处理预料之中的错误是一种良好的编程习惯,但有时候因为各种原因也会选择忽略错误(主要是因为懒),而避免“err != nil”的操作笔者目前只用到以下这种方法:
xx, _ = func(xx)
也就是用“_”直接忽略了传送的err参数,但如果有时间还是尽量进行错误处理,毕竟golang如此设计的初衷便是让程序员正视错误,并解决错误。
此外,还有一些有趣的方法可以避免重复的err操作,详情见:如何减少重复err