3

To panic or not to panic

 3 years ago
source link: https://blog.huoding.com/2019/03/27/725
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

To panic or not to panic

发表于2019-03-27

大家都知道 Golang 推荐的错误处理的方式是使用 error,这主要得益于 Golang 方法可以返回多个值,我们可以很自然的用最后一个值来表示是否有错误,这一点是其它很多编程语言所不具备的,不过这多少让那些习惯了 exception 的程序员无所适从,虽然 Golang 没有 exception,但是实际上可以通过 panic/recover 来模拟出类似的效果,于是很多 Gopher 在错误处理的时候开始倾向于直接 panic。

为什么会有人喜欢使用 panic 来处理错误呢?我们以流行框架 Gin 为例来说明:

package main

import (
	"errors"
	"log"
	"net/http"

	"github.com/gin-gonic/gin"
)

func recovery() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Next()

		if err := c.Errors.Last(); err != nil {
			c.String(http.StatusInternalServerError, err.Error())
		}
	}
}

func main() {
	r := gin.Default()
	r.Use(recovery())

	r.GET("/", func(c *gin.Context) {
		if c.Query("foo") != "" {
			err := errors.New("foo error")

			c.Error(err)
			return
		}

		if c.Query("bar") != "" {
			err := errors.New("bar error")

			c.Error(err)
			return
		}

		c.String(http.StatusOK, "test")
	})

	r.Run(":8080")
}

在 Gin 的用法中,当出错的时候,应该先调用 c.Error 方法来设置 error,如果是在中间件里,那么应该调用 c.AbortWithError 方法,最后还要记得调用 return 返回,后续可以在中间件中通过判断 c.Errors 来决定如何渲染状态码和错误信息。很多人会觉得先 c.Error 再 return 的操作太麻烦,于是就出现了直接 panic 的做法:

package main

import (
	"fmt"
	"net/http"

	"github.com/gin-gonic/gin"
)

func recovery() gin.HandlerFunc {
	return func(c *gin.Context) {
		defer func() {
			if err := recover(); err != nil {
				c.String(http.StatusInternalServerError, fmt.Sprint(err))
			}
		}()

		c.Next()
	}
}

func main() {
	r := gin.Default()
	r.Use(recovery())

	r.GET("/", func(c *gin.Context) {
		if c.Query("foo") != "" {
			panic("foo error")
		}

		if c.Query("bar") != "" {
			panic("bar error")
		}

		c.String(http.StatusOK, "test")
	})

	r.Run(":8080")
}

也就是说,当出错的时候,直接 panic 抛出异常,然后在中间件里通过 recover 里捕获异常,进而决定如何渲染错误信息,业务逻辑代码会更简洁明了。

如此说来,在 Golang 错误处理的时候,到底是应该使用 error 还是 panic 呢?之所有会有这样的疑问,很大程度上是因为我们混淆了错误和异常的区别:一个例子,当操作一个文件但是文件却不存在的时候,应该使用 error 而不是 panic,因为文件不存在可能在很多情况下新建一个就可以了,此时有药可救;另一个例子,当除数是零的时候,应该使用 panic 而不是 error,因为除数为零在数学上无意义,此时无药可救。

顺着这个思路,比如说在一个 MVC 架构的 Web 应用里,如果我们想在 controller 里报错,那么最终一般是展示一个定制化的错误页面,此时看上去属于无药可救的范畴,如此说来即便使用 panic 似乎也无可厚非,不过如果是在 model 之类可复用的组件中报错的话,除非真的真的无药可救,否则应该尽可能使用 error,毕竟你不可能指望别人在复用组件的时候还搭配着 recover 兜底。

此外,一旦在错误处理的时候滥用 panic,那么很可能会导致你忽略真正的 panic,比如当你的 Web 应用存在一个偶发崩溃问题的时候,而你却只是使用 panic/recover 渲染了一个错误页面,那么你很可能就错失这个问题了,当然,你可以在 recover 里记录日志信息,不过当你滥用 panic 的时候,即便记录日志信息,也会存在很多噪音,结局你很可能依然会错失真正有用的信息。问题的症结就在于混淆了错误和异常。

实际上,针对此类问题,Gin 作者有过相关的论述:Abort vs panic,Golang 官方博客中的文章也值得多读几遍:Error handling and GoErrors are values

综上所述,我们推荐 error 为主,panic 为辅。如果一定要 panic,最好是在 init 的时候 panic,毕竟一运行就看到挂掉比较容易发现并处理,对待 panic,务必要克制,它就像罂粟花,看似绚烂多彩,却隐藏着罪恶的果实,合理使用的话,有其自身价值,但千万别上瘾。

此条目由老王发表在Technical分类目录,并贴了Golang标签。将固定链接加入收藏夹。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK