Go中的"优雅"的错误处理 | Go 主题月

1,743 阅读7分钟

前言

在这愉快的周末,在大家都出去玩的时候,我选择留下来,输出一波文章,不为别的,就只为了那一只——大茶缸

那么,今天我要说的是在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)
	}
}

那么在报错上就会变成这样,而不是整个服务直接宕掉了。

WX20210329-115056.png

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了,而变成这样,同时在命令行里面也是报了一堆红

WX20210329-120331.png

这不是我们想要的,我们希望看到一些清晰明了的错误信息,为了达到这种目的,就需要使用到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里面同样也是使用了这个来保护端口在发生错误的时候,不会给退出了。

WX20210329-164942.png

总结

虽然Go的错误处理机制非常的繁琐,而且没有类似于try...catch这样的一把梭的方式处理,但是这样的方式却也可以帮助我们写出更加健壮的代码,同时在处理的时候,也可以方便我们快速定位到错误的位置,最后阅读性也比直接的一把梭哈要来的好很多,就是在开发阶段会比较繁琐而已。

我是一个还在学Go的小菜鸡,让我们一起加油,为了大茶缸