0%

defer+recover保护gin不panic

本文记录了作者保护gin构建的web app不panic的方式,简单来说:

  1. 主程中的panic本身是会被gin拦截的
  2. 协程中的panic需要使用deferrecover进行保护

情景

在我们用gin构建,运行web app并上线了之后,或许有一些请求会经过业务,在特定的情况下出发会触发golang中的panic

按照golang的设定,一旦panic,如果不在函数调用栈中存在recover,那么是一定会使得整个程序终止的

但是线上的服务是不能够因为一个两个的请求就直接终止了的,这样非常危险,所以我们需要手段来阻止web app在panic的情况下直接终止

解决方案

主程序中的panic

对于gin这个web框架来说,主程序中的panic是会被自动recover的,还会打印出非常详细的日志信息,比如

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

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

func main() {
r := gin.Default()
r.GET("/panic", func(ctx *gin.Context) {
panic("panic")
})
r.Run()
}

运行之后我们作如下HTTP请求

1
> curl localhost:8080/panic

会发现在gin的运行窗口出现如下信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2020/06/04 18:42:12 [Recovery] 2020/06/04 - 18:42:12 panic recovered:
GET /panic HTTP/1.1
Host: localhost:8080
Accept: */*
User-Agent: curl/7.64.1


panic
/Users/admin/go/src/gin-test/main.go:10 (0x1581418)
main.func1: panic("panic")
/Users/admin/go/src/github.com/gin-gonic/gin/context.go:165 (0x156baca)
(*Context).Next: c.handlers[c.index](c)
/Users/admin/go/src/github.com/gin-gonic/gin/recovery.go:83 (0x157fb13)
RecoveryWithWriter.func1: c.Next()
/Users/admin/go/src/github.com/gin-gonic/gin/context.go:165 (0x156baca)
(*Context).Next: c.handlers[c.index](c)
/Users/admin/go/src/github.com/gin-gonic/gin/logger.go:241 (0x157ec40)
LoggerWithConfig.func1: c.Next()
/Users/admin/go/src/github.com/gin-gonic/gin/context.go:165 (0x156baca)
(*Context).Next: c.handlers[c.index](c)
/Users/admin/go/src/github.com/gin-gonic/gin/gin.go:420 (0x1575d20)
(*Engine).handleHTTPRequest: c.Next()
/Users/admin/go/src/github.com/gin-gonic/gin/gin.go:376 (0x157548c)
(*Engine).ServeHTTP: engine.handleHTTPRequest(c)
/usr/local/Cellar/go/1.13.8/libexec/src/net/http/server.go:2802 (0x12cb6d3)
serverHandler.ServeHTTP: handler.ServeHTTP(rw, req)
/usr/local/Cellar/go/1.13.8/libexec/src/net/http/server.go:1890 (0x12c6f74)
(*conn).serve: serverHandler{c.server}.ServeHTTP(w, w.req)
/usr/local/Cellar/go/1.13.8/libexec/src/runtime/asm_amd64.s:1357 (0x105c030)
goexit: BYTE $0x90 // NOP

[GIN] 2020/06/04 - 18:42:12 | 500 | 1.238546ms | ::1 | GET "/panic"

并且整个app还在正常运行,没有终止,这非常好

协程中的panic

不过非常可惜的是,对于协程中的panic,gin并不能做到自动recover并打印日志信息,比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

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

func main() {
r := gin.Default()
r.GET("/go-panic", func(ctx *gin.Context) {
go func() {
panic("panic")
}()
})
r.Run()
}

运行该app之后,我们作如下的HTTP请求

1
> curl localhost:8080/go-panic

会发现gin app退出了

1
2
3
4
5
6
7
8
panic: panic

goroutine 24 [running]:
main.main.func1.1()
/Users/admin/go/src/gin-test/main.go:11 +0x39
created by main.main.func1
/Users/admin/go/src/gin-test/main.go:10 +0x35
exit status 2

协程解决方案

所以,对于协程,我们要手动进行deferrecover,来避免app的退出和打印日志信息,比如上面的代码应该修改为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"

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

func main() {
r := gin.Default()
r.GET("/go-panic", func(ctx *gin.Context) {
go func() {
defer func() {
if err := recover(); err != nil {
fmt.Printf("error: %v\n", err)
}
}()
panic("panic")
}()
})
r.Run()
}

而后我们像刚才一样进行HTTP请求

1
> curl localhost:8080/go-panic

会得到如下打印

1
2
error: panic
[GIN] 2020/06/04 - 18:50:20 | 200 | 2.951µs | ::1 | GET "/go-panic"

可以看到app正常响应了请求,并且没有退出并打印了日志,想要更多定制操作可以修改defer的函数