对于golang进程出现crash,大家都知道使用recover,但是对于http server这种流程,recover应该怎么加呢?
一般会想到:
package main
import (
"errors"
"fmt"
"github.com/gorilla/mux"
"log"
"net/http"
)
func main() {
defer func() { // 必须要先声明defer,否则不能捕获到panic异常
glog.Infoln("d")
if err := recover(); err != nil {
glog.Infoln(err) // 这里的err其实就是panic传入的内容
}
glog.Infoln("e")
}()
rtr := mux.NewRouter()
rtr.HandleFunc("/", withPanic).Methods("GET")
http.Handle("/", rtr)
log.Println("Listening...")
http.ListenAndServe(":3001", http.DefaultServeMux)
}
func withPanic(w http.ResponseWriter, r *http.Request) {
panic("crash")
}
但是当你run起来之后,只要访问3001,就会直接崩掉。。。。。。。咋没被recover捕捉到呢?原来是http.ListenAndServe()函数的实现中调用了函数Serve,而Serve函数的逻辑里,对于处理http消息的部分是通过协程操作的。。。
func (srv *Server) Serve(l net.Listener) error {
if fn := testHookServerServe; fn != nil {
fn(srv, l) // call hook with unwrapped listener
}
l = &onceCloseListener{Listener: l}
defer l.Close()
if err := srv.setupHTTP2_Serve(); err != nil {
return err
}
if !srv.trackListener(&l, true) {
return ErrServerClosed
}
defer srv.trackListener(&l, false)
var tempDelay time.Duration // how long to sleep on accept failure
baseCtx := context.Background() // base is always background, per Issue 16220
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {
rw, e := l.Accept()
if e != nil {
select {
case <-srv.getDoneChan():
return ErrServerClosed
default:
}
if ne, ok := e.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
time.Sleep(tempDelay)
continue
}
return e
}
tempDelay = 0
c := srv.newConn(rw)
c.setState(c.rwc, StateNew) // before Serve can return
go c.serve(ctx) -------------------->原因所在!
}
}
也就是说,对于用户访问api的响应,是由协程处理,那么自然协程的panic不会被主进程捕获了。
于是可以想到的正确姿势:
package main
import (
"errors"
"github.com/gorilla/mux"
"log"
"net/http"
)
func main() {
m := mux.NewRouter()
m.Handle("/", RecoverWrap(http.HandlerFunc(handler))).Methods("GET")
http.Handle("/", m)
log.Println("Listening...")
http.ListenAndServe(":3001", nil)
}
func handler(w http.ResponseWriter, r *http.Request) {
panic(errors.New("panicing from error"))
}
func RecoverWrap(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var err error
defer func() {
r := recover()
if r != nil {
switch t := r.(type) {
case string:
err = errors.New(t)
case error:
err = t
default:
err = errors.New("Unknown error")
}
sendMeMail(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}()
h.ServeHTTP(w, r)
})
}
func sendMeMail(err error) {
// send mail
}
这样,即使程序panic了,也不会导致整个程序崩掉,而是会重新启动。
参考:https://stackoverflow.com/questions/28745648/global-recover-handler-for-golang-http-panic
有疑问加站长微信联系(非本文作者)