Go 的 defer + recover 兜住 panic:中间件里做崩溃恢复,以及哪些 panic 你根本救不回来
Go 的 defer recover 兜住 panic:中间件里做崩溃恢复,以及哪些 panic 你根本救不回来线上服务跑得好好的,某个请求触发了一个 nil 指针解引用,整个进程直接挂掉——这是很多 Go 新手第一次被 panic 教育的场景。Go 不像 Java 有个兜底的线程级异常处理,一个没被 recover 的 panic 会顺着调用栈一路炸到main,把整个进程带走。这篇讲怎么用defer recover正确地兜住 panic,以及哪些 panic 你 recover 也救不回来。先看错误的写法:recover 放错位置很多人第一次写 recover,是这样的:funchandle(){recover()// 没用!recover 不在 defer 里,永远返回 nilmayPanic()}recover()只有在defer 函数内直接调用时才有效。上面这行在正常流程里执行,此时没有 panic 正在传播,它只会返回nil,等于白写。还有一种更隐蔽的错法——把 recover 包了一层:funcsafeRecover(){recover()// 也没用!它不是被 defer 直接调用的}funchandle(){defersafeRecover()// defer 的是 safeRecover,recover 隔了一层调用栈mayPanic()}recover必须由 defer直接调用的那个函数来执行,中间隔一层普通函数调用就失效。记住这条铁律,能省掉一半的坑。正确写法:defer 里的匿名函数funchandle()(errerror){deferfunc(){ifr:recover();r!nil{// 把 panic 转成 error 往上返回,而不是让进程死errfmt.Errorf(panic recovered: %v,r)}}()mayPanic()returnnil}关键点:recover()写在defer func(){...}()内部,直接调用。捕获到之后,通常做两件事——记录日志(带上堆栈)、把 panic 转成 error 返回。注意函数用了具名返回值err,这样才能在 defer 里修改它;如果是匿名返回值,defer 里改了外面也拿不到。实战:一个 HTTP recover 中间件真正有用的场景是给整个服务加一层兜底,让单个 handler 的 panic 不至于拖垮进程:funcRecover(next http.Handler)http.Handler{returnhttp.HandlerFunc(func(w http.ResponseWriter,r*http.Request){deferfunc(){iferr:recover();err!nil{// debug.Stack() 拿到完整堆栈,定位问题全靠它log.Printf(panic: %v\n%s,err,debug.Stack())http.Error(w,Internal Server Error,http.StatusInternalServerError)}}()next.ServeHTTP(w,r)})}用的时候套在最外层:mux:http.NewServeMux()mux.HandleFunc(/api,apiHandler)http.ListenAndServe(:8080,Recover(mux))这样任何 handler 里的 panic 都会被这层中间件接住,返回 500 而不是让进程崩溃。debug.Stack()是排查关键,只记录err本身你根本不知道 panic 在哪一行。坑:每个 goroutine 都要自己 recover这是最容易翻车的地方。recover只能兜住当前 goroutine 的 panic,你在 handler 里起了个新 goroutine,它 panic 了,外层中间件的 recover 完全接不住:funchandler(w http.ResponseWriter,r*http.Request){gofunc(){mayPanic()// 这个 panic 会直接杀掉整个进程!中间件的 recover 救不了}()}正确做法是给每个手动起的 goroutine 都配一个 recover:funcSafeGo(fnfunc()){gofunc(){deferfunc(){ifr:recover();r!nil{log.Printf(goroutine panic: %v\n%s,r,debug.Stack())}}()fn()}()}哪些「panic」你 recover 也救不回来recover 不是万能的。有几类是fatal error,不走 panic 机制,recover 完全无效,进程必死:并发读写 map:fatal error: concurrent map read and map write。这是运行时直接抛的 fatal error,不是 panic,recover 接不住。栈溢出:无限递归导致fatal error: stack overflow。死锁:所有 goroutine 都阻塞时fatal error: all goroutines are asleep - deadlock。显式os.Exit():直接退出,连 defer 都不执行。验证一下并发 map 救不回来:funcmain(){deferfunc(){// 这个 recover 对下面的并发 map 写完全无效ifr:recover();r!nil{fmt.Println(recovered:,r)}}()m:map[int]int{}fori:0;i10;i{gofunc(){for{m[1]1// 并发写,触发 fatal error,进程直接死}}()}select{}}运行会打印fatal error: concurrent map read and map write然后崩溃,那句recovered:永远不会出现。这类问题的正解是从根上消除竞态(加锁、sync.Map或分片),而不是指望 recover。小结recover()必须在defer里直接调用才有效,隔一层普通函数就失效。用具名返回值(err error),才能在 defer 里把 panic 转成 error 返回。HTTP 服务加一层 recover 中间件兜底,配合debug.Stack()记录堆栈。每个手动起的 goroutine 都要自己 recover,外层接不住子 goroutine 的 panic。并发读写 map、栈溢出、死锁是fatal error,recover 无效,只能从根上避免。一句话记忆点:recover 兜的是「panic」,兜不了「fatal error」;而且它只兜自己所在的那个 goroutine。