Go 安全使用goroutine
go 正常使用goroutine开启一个携程很简单
var a int
go func(){
a=1+1
}()
这么用在日常工具什么的开发中肯定没问题,如果携程内有问题崩掉了,使用工具的人可以马上获得堆栈信息将其反应给开发人员。但是你如果在web服务器或者后台程序中使用就有大问题。因为golang无法捕获携程中的panic,也就是说你携程崩掉了,你携程中又没有recover,你整个程序都会被其带崩,并且崩溃是父携程不可捕获的。
上案例
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
app := gin.New()
app.Handle(http.MethodGet, "/panic", func(ctx *gin.Context) {
go func() { panic("goroutine panic") }()
})
app.Handle(http.MethodPost, "/panic", func(ctx *gin.Context) { panic("panic") })
http.ListenAndServe("0.0.0.0:7999", app)
}
此处使用gin框架做示范,模拟了两种崩溃情况.此时分别以get和post两种方法请求/panic接口,get请求程序,程序会因为无法捕获携程中的panic会崩溃,而post请求会恢复,因为golang 处理http请求时,在携程中使用了recover,你只要用http.ListenAndServe就能保你。但是你如果再自己的handlefunc 中再开子携程且没有上保护措施那么就保不住你啰
编写一个安全的goroutine 构建器
type Trace struct {
info any
stack string
}
type RoutineBuilder struct {
out io.Writer
traceChan chan Trace
}
func (rb *RoutineBuilder) Go(fun func()) {
go func(f func()) {
defer func() {
//记录panic原因,以及堆栈信息用于排查
if r := recover(); r != nil {
rb.traceChan <- Trace{
info: r,
stack: string(debug.Stack()),
}
}
}()
f()
}(fun)
}
func NewRoutineBuilder(out io.Writer) (*RoutineBuilder, <-chan Trace) {
var rb = RoutineBuilder{
out: out,
traceChan: make(chan Trace, 20),
}
return &rb, rb.traceChan
}
应用该携程构建器到案例中
这里的策略为将意外崩溃的携程信息直接写到标准输出,日志处理那步可以根据自身需求对堆栈信息做处理
package main
import (
"fmt"
"io"
"net/http"
"os"
"runtime/debug"
"time"
"github.com/gin-gonic/gin"
)
type Trace struct {
info any
stack string
}
type RoutineBuilder struct {
out io.Writer
traceChan chan Trace
}
func (rb *RoutineBuilder) Go(fun func()) {
go func(f func()) {
defer func() {
if r := recover(); r != nil {
rb.traceChan <- Trace{
info: r,
stack: string(debug.Stack()),
}
}
}()
f()
}(fun)
}
func NewRoutineBuilder(out io.Writer) (*RoutineBuilder, <-chan Trace) {
var rb = RoutineBuilder{
out: out,
traceChan: make(chan Trace, 20),
}
return &rb, rb.traceChan
}
var routineBuilder, stackChan = NewRoutineBuilder(os.Stderr)
//记录意外崩溃的堆栈信息和原因到日志中
func traceLogCheck() {
defer func() {
if r := recover(); r != nil {
fmt.Fprintln(routineBuilder.out, time.Now(), " trace log check panic", r, string(debug.Stack()))
traceLogCheck()
}
}()
for info := range stackChan {
fmt.Fprintln(routineBuilder.out, time.Now(), " trace log check panic", info.info, info.stack)
}
}
func init() {
go traceLogCheck()
}
func main() {
app := gin.New()
app.Handle(http.MethodGet, "/panic", func(ctx *gin.Context) {
routineBuilder.Go(func() { panic("goroutine panic") })
})
app.Handle(http.MethodPost, "panic", func(ctx *gin.Context) { panic("panic") })
http.ListenAndServe("0.0.0.0:7999", app)
}
看看效果
成功拦截到handfunc中子携程的panic