这一章节是实现错误处理机制,是通过中间件形式来实现的。
panic
Go 语言中,比较常见的错误处理方法是返回 error,由调用者决定后续如何处理。但是如果是无法恢复的错误,就会触发panic。用户也可以手动触发 panic,当然如果在程序运行过程中出现了类似于数组越界的错误,panic 也会被触发。
panic 会中止当前执行的程序,退出。
defer
panic 会导致程序被中止,但是在退出前,会先处理完当前协程上已经defer 的任务,执行完成后再退出。
可以 defer 多个任务,在同一个函数中 defer 多个任务,会逆序执行。即先执行最后 defer 的任务。在这里,defer 的任务执行完成之后,panic 还会继续被抛出,导致程序非正常结束。
func main() {defer fmt.Println("defer func")arr := []int{1, 2}fmt.Println(arr[3])
}
recover
Go 语言还提供了 recover 函数,可以避免因为 panic 发生而导致整个程序终止,recover 函数只在 defer 中生效。
func main() {test_recover()//test_recover2() //该函数内有两个defer函数fmt.Println("after recvoer")
}func test_recover() {defer func() {fmt.Println("defer func")if err := recover(); err != nil {fmt.Println("recvoer success")}}()arr := []int{1, 2}fmt.Println(arr[3]) //在该句发生painc,就会交由defer处理,下面的"after painc"不会打印fmt.Println("after painc")
}func test_recover2() {defer func() {fmt.Println("defer net/http")if err := recover(); err != nil {fmt.Println("net/http recover success")}}()defer func() {fmt.Println("defer 中间件")if err := recover(); err != nil {fmt.Println("中间件 recvoer success")}}()arr := []int{1, 2}fmt.Println(arr[3])fmt.Println("after painc")
}
test_recover结果
可以看到,recover 捕获了 panic,程序正常结束。当 panic 被触发时,控制权就被交给了 defer 。而在 main() 中打印了 after recover,说明程序已经恢复正常,继续往下执行直到结束。
$ go run main.go
defer func
recover success
after recover
test_recover2函数的效果可自行测试。
web框架的错误处理机制
对一个 Web 框架而言,错误处理机制是很有必要的。可能是框架本身没有完备的测试,导致在某些情况下出现空指针异常等情况。也有可能用户不正确的参数,触发了某些异常,例如数组越界,空指针等。如果因为这些原因导致系统宕机,必然是不可接受的。
首先是要明确知道net/http是实现了recover机制捕获的。
//源码1.21.3 src/net/http/server.go文件
//3026行, accept一个新连接,并开启一个协程处理该新连接
func (srv *Server) Serve(l net.Listener) error {for {rw, err := l.Accept()......................go c.serve(connCtx)}
}// 1857行
// Serve a new connection.
func (c *conn) serve(ctx context.Context) {............defer func() {if err := recover(); err != nil && err != ErrAbortHandler {....................}}()for {serverHandler{c.server}.ServeHTTP(w, w.req)}
}
我们将在该web框架中添加一个非常简单的错误处理机制,即在此类错误发生时,向用户返回 Internal Server Error,并且在日志中打印必要的错误信息,方便进行错误定位。
我们之前实现了中间件机制,错误处理也可以作为一个中间件,增强该web框架的能力。
中间件 Recovery代码实现
func Recovery() HandlerFunc {return func(c *Context) {defer func() {if err := recover(); err != nil {message := fmt.Sprintf("%s", err)log.Printf("%s\n\n", trace(message))c.Fail(http.StatusInternalServerError, "Internal Server Error")}}()c.Next() //可以试试注释该行代码,看看效果}
}
// print stack trace for debug
func trace(message string) string {var pcs [32]uintptrn := runtime.Callers(3, pcs[:]) // skip first 3 callervar str strings.Builderstr.WriteString(message + "\nTraceback:")for _, pc := range pcs[:n] {fn := runtime.FuncForPC(pc)file, line := fn.FileLine(pc)str.WriteString(fmt.Sprintf("\n\t%s:%d", file, line))}return str.String()
}
Recovery 的实现非常简单,使用 defer 挂载上错误恢复的函数,在这个函数中调用 recover(),捕获 panic,并且将堆栈信息打印在日志中,向用户返回 Internal Server Error,其中 trace() 函数是用来获取触发 panic 的堆栈信息。(可以不用过于深入了解trace函数,我们主要是要实现这个错误处理机制。不使用第六行代码也可以实现该错误处理机制,只是没有打印出来错误的堆栈信息)。
trace函数:
在 trace() 中,调用了 runtime.Callers(3, pcs[:])
,Callers 用来返回调用栈的程序计数器, 第 0 个 Caller 是 Callers 本身,第 1 个是上一层 trace,第 2 个是再上一层的 defer func
。因此,为了日志简洁一点,我们跳过了前 3 个 Caller。接着,通过 runtime.FuncForPC(pc)
获取对应的函数,在通过 fn.FileLine(pc)
获取到调用该函数的文件名和行号,打印在日志中。
再说回Recovery的实现,重点讲讲第11行的c.Next()。要是不使用该行代码的话,该中间件就立即结束了,就会执行defer中的函数,那要是出现了panic的错误的话,这里就不能恢复。
使用该行代码的话,那该中间件就一直在执行。等到执行路由Handler时,要是此时出现panic错误的话,控制权就被交给了该中间件的defer,这样就可以进行错误恢复了,不会宕机。
其实不使用也不会宕机的,因为net/http是实现了recover机制捕获的。
疑问:net/http是实现了recover机制捕获,为什么web框架还要自定义recver机制捕获?
自定义 recovery 中间件是一种常见的做法,用于捕获并处理应用程序的运行时错误,以避免整个应用程序崩溃并返回对应格式的响应数据。
我认为主要是可以返回对应格式的响应数据。
按照下面的测试例子,不使用自写的recovery中间件的话,在浏览器访问会显示 无法显示此网页。
而使用recovery中间件,那可以显示{"message":"Internal Server Error"},即是返回我们给定的格式的响应数据。
测试
func main() {r := gee.Default() //Default是使用了recovery中间件的r.GET("/", func(c *gee.Context) {c.String(http.StatusOK, "Hello Gee\n")})// index out of range for testing Recovery()r.GET("/panic", func(c *gee.Context) {names := []string{"gee"}c.String(http.StatusOK, names[100])})r.Run("localhost:10000")
}
思考:在其他协程中发生了panic,会被recover捕捉到吗?
先思考一个问题。在没有使用自写的recovery中间件的时候,触发panic情况时候,是net/http中的recover捕获了panic。而在使用自写的recovery中间件时,是其中间件捕获了panic。这是怎样的?
这就回到recover中的test_recover2函数了。再回顾下该函数,其内有两个defer函数。
func test_recover2() {defer func() {fmt.Println("defer net/http")if err := recover(); err != nil {fmt.Println("net/http recover success")}}()defer func() {fmt.Println("defer 中间件")if err := recover(); err != nil {fmt.Println("中间件 recvoer success")}}()arr := []int{1, 2}fmt.Println(arr[3])fmt.Println("after painc")
}
这就很像是使用了recvoery中间件的例子。第一个defer的函数就是net/http中的serve方法的defer recvoer,而第二个defer就是中间件的defer。
多个defer可以想象成是栈结构,先进后出嘛。这结果就是中间件的recover捕获到了panic。
再回到该主题,在其他协程触发了panic,中间件或者net/http会捕获到吗?回答是不会。
在 Go 语言中,我们无法在父协程里捕获子协程的异常(panic)。如果服务有异常没有处理,整个进程都会退出。
net/http和中间件的recover都是作用在路由Handler所在的协程的。
来看例子:
func main() {r := gee.Default() //Default是使用了recovery中间件的// index out of range for testing Recovery()r.GET("/panic", func(c *gee.Context) {names := []string{"geek"}c.String(http.StatusOK, names[100])})//开启子线程go func() {arr := []int{1, 2}fmt.Println(arr[4])}()r.Run("localhost:10000")
}
结果:开启新协程,recovery中间件是捕获不到其他协程的panic。
再看另一种情况,在路由Handler协程中开启新协程
func main() {r := gee.Default() //Default是使用了recovery中间件的r.GET("/bad", func(c *gee.Context) {//在路由Handler协程中去开启新线程go func() {arr := []int{1, 2}fmt.Println(arr[4])}()c.String(http.StatusOK, "hello")})r.Run("localhost:10000")
}
结果:也一样是捕获不到panic的。
所以,在其他协程触发了panic,中间件或者net/http不会捕捉到的,那整个进程就会退出。
所以,不能认为gin框架不会宕机的。在开启新协程中有某些操作使用不当的话,会触发panic,那就会宕机的。
因此,尽量少在路由Handler协程中再开启新协程;或者开启新协程后,需要谨慎处理。
完整代码:https://github.com/liwook/Go-projects/tree/main/gee-web/7-recover