前言
在这愉快的周末,在大家都出去玩的时候,我选择留下来,输出一波文章,不为别的,就只为了那一只——大茶缸。
那么,今天我要说的是在Go中,对于错误处理这个事情,在Go中的错误处理跟我Javascript这些有很多的不一样,那么下面我就来聊一下,都有哪些不一样。
资源管理和错误处理
首先在介绍错误处理前,必须要一起说的一点就是资源管理,很多时候除了一些语法上的低级错误,更多的时候,就是在读写一些资源文件的时候,出现的一些错误,所以这里就先来聊一聊资源管理的问题。
例如有这么一个需求,拿到网上的一些数据,然后把它输出成一个文件出来,这个文件可以通过接口来进行访问,这就是一个最基本的读写任务了。
那么我们首先来看一下,把一些数据输出成一个文件,在Go里面是怎么样做的呢
func writeFile(filename string) {
file, e := os.Create(filename)
if e != nil {
panic(e)
}
defer file.Close() //关闭
writer := bufio.NewWriter(file)
defer writer.Flush() //将内存写入
for i := 0; i < 10; i++ {
_, _ = fmt.Fprintln(writer, i)
}
}
func main() {
writeFile("test.txt")
}
这里可以看到我使用了一个语法——defer,那么这个语法有什么作用的呢
什么defer
其实defer使用起来,有点像koa2里面的洋葱模型,它会在这个函数执行完成的时候,defer后面的语句,而且它的执行顺序,是先进后出的栈。
也就是下面这样
func test2() {
fmt.Println(1)
fmt.Println(2)
defer fmt.Println(3)
fmt.Println(4)
defer fmt.Println(5)
}
//执行结果 1 2 4 5 3
它在运用的时候 ,最大的好处,就是可以在写代码的时候,不用跟着逻辑走,例如,上面的写入文件的例子,可以在创建create的时候,下面立马跟上defer close,而不需要等后续写入完成之后去close,这样就不怕因为后面写着,而把这一步给忘记了。
沉重的panic
在不同的语言里面,都会有不同的弹出错误的方式,例如,在Javascript中,是通过throw来完成这样的一件事,而在Go中,同样也存在这样的一个语法,它就是——panic。
在使用上也是非常的简单
func throw() error {
e := errors.New("this is err")
return e
}
func main() {
e :=throw()
panic(e)
}
但是很多时候,我们并不能这样子去处理一个报错的内容,因为panic会使得一个程序直接给宕掉,虽然在http listen的时候,存在一个保护机制(这个后面再聊),但是总的体验不太好,所以一般上来说对于一些已知的错误,我们不会直接的去panic。
有一点需要注意的:那就是就算Panic之后,也会一层层去执行函数里面的defer,这一点非常重要。
优雅的报错
很多时候,我们在处理一些错误信息的时候,会有一个总的错误验证函数,这个函数接受所有的错误信息来统一处理。
例如要读取上面写入的txt文件,我们一般上来说是这样的
func handleData(writer http.ResponseWriter,
request *http.Request) {
path := request.URL.Path[len("/test/"):]
file, e := os.Open(path)
if e != nil {
panic(e)
}
defer file.Close()
bytes, e := ioutil.ReadAll(file)
if e != nil {
panic(e)
}
writer.Write(bytes)
}
func main() {
http.HandleFunc("/test/", handleData)
serve := http.ListenAndServe(":8000", nil)
if serve != nil {
panic(serve)
}
}
上面的例子中,我选择了直接抛出错误,但是这样很不友好,如果我们有一个总的错误处理机制,来接受所有的错误问题,而不需要逻辑函数来进行错误处理,而逻辑函数只需要把错误给抛出就行了。
那么整体的实现思路就会变成下面这样子
type AppHandleError func(writer http.ResponseWriter,
request *http.Request) error
func WebError(handleError AppHandleError) func(writer http.ResponseWriter,
request *http.Request) {
return func(writer http.ResponseWriter, request *http.Request) {
err := handleError(writer, request)
if err != nil {
switch { //这里对每个不同的错误进行不同的处理
case os.IsNotExist(err):
http.Error(writer, http.StatusText(http.StatusNotFound),
http.StatusNotFound)
default:
http.Error(writer, http.StatusText(http.StatusInternalServerError),
http.StatusInternalServerError)
}
}
}
}
这里做了一些小小的改动,把错误给统一的返回回去,由外部错误处理器去进行处理,这样在逻辑代码上,就可以把错误处理这一块给抽象出来了。
func handleData(writer http.ResponseWriter,
request *http.Request) error {
path := request.URL.Path[len("/test/"):]
file, e := os.Open(path)
if e != nil {
return e
}
defer file.Close()
bytes, e := ioutil.ReadAll(file)
if e != nil {
return e
}
writer.Write(bytes)
return nil
}
func main() {
http.HandleFunc("/test/", appError.WebError(handleData)) //用总的错误处理包起来
serve := http.ListenAndServe(":8000", nil)
if serve != nil {
panic(serve)
}
}
那么在报错上就会变成这样,而不是整个服务直接宕掉了。
Go中的“try...catch...”
相信对于try...catch... ,这个对于我们来说是在熟悉不过的一个东西,这也是一种我们常用的错误处理方式之一,而在Go中,没有try...catch... 语法,但是却有类似的东西,那就是——recover,这是一种跟try...catch... 实现同样功能的一种Go语法。
recover还有一个非常重要的功能,那就是可以保护进程在出错的时候,不会宕掉,而http.listen也是通过这个来实现进程保护的。
这里我们同样用上面那个案例作为例子,例如在外部我们监听的不是/test/,而只是单纯的一个/。
func main() {
http.HandleFunc("/", appError.WebError(handleData)) //这里路由换了
serve := http.ListenAndServe(":8000", nil)
if serve != nil {
panic(serve)
}
}
这个时候我们发出错误请求的时候,他就不会是404了,而变成这样,同时在命令行里面也是报了一堆红
这不是我们想要的,我们希望看到一些清晰明了的错误信息,为了达到这种目的,就需要使用到recover ,在进程中,如果出现了panic这种报错,如果没有一个recover去接住这个错误的话,程序是会直接推出的。
recover的使用说明
这里先来说明一下recover的具体使用,先来个简单的🌰
func ThrowErr() {
defer func() {
r := recover()
if e,file := r.(error); file {
fmt.Print("this is error",e) // this is error
} else {
panic("this is I down know error")
}
}()
panic(errors.New("this is error"))
}
这里的defer接受一个匿名函数,用来最后执行,而在这个匿名函数里面又一个recover用来做兜底的工作,我们可以看到,当我们返回的一个错误是一个可以识别的错误的时候,他会在命令行里面直接打出这个错误内容,而不是给我报一堆红。
当然,如果是一个无法识别的error,那也是直接把这个错误通过panic给抛出了。
结合例子,再次总结
这里我结合上面的读文件的例子,来使用recover,具体实施起来是长这样的
func WebError(handleError AppHandleError) func(writer http.ResponseWriter,
request *http.Request) {
return func(writer http.ResponseWriter, request *http.Request) {
defer func() {
r := recover()
if e,ok := r.(error); ok {
fmt.Print("this is error \n",e)
http.Error(writer, "please check /",http.StatusNotFound)
} else {
panic(e)
}
}()// 只需要补充一个兜底的处理
err := handleError(writer, request)
if err != nil {
switch {
case os.IsNotExist(err):
http.Error(writer, http.StatusText(http.StatusNotFound),
http.StatusNotFound)
default:
http.Error(writer, http.StatusText(http.StatusInternalServerError),
http.StatusInternalServerError)
}
}
}
}
只需要一个简单的recover就可以避免你的进程给宕掉了,而在http listen里面同样也是使用了这个来保护端口在发生错误的时候,不会给退出了。
总结
虽然Go的错误处理机制非常的繁琐,而且没有类似于try...catch这样的一把梭的方式处理,但是这样的方式却也可以帮助我们写出更加健壮的代码,同时在处理的时候,也可以方便我们快速定位到错误的位置,最后阅读性也比直接的一把梭哈要来的好很多,就是在开发阶段会比较繁琐而已。
我是一个还在学Go的小菜鸡,让我们一起加油,为了大茶缸。