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