golang生态的错误处理机制和常见的面向对象语言不尽相同。通常情况下,我们可以将panicrecover机制结合起来处理程序运行期间发生的异常错误。
不过这也不是万能的,在语言层面有一些致命的错误是无法被recover捕获的。
本篇文章将会介绍常见的可恢复和不可恢复两大类Panic。

可恢复Panic

显式触发的Panic

func safeOperation() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered:", r)
        }
    }()
    panic("manual panic trigger")
}

运行时错误

空指针解引用

var ptr *int
fmt.Println(*ptr) // runtime error: invalid memory address or nil pointer dereference

数组/切片越界

arr := []int{1}
fmt.Println(arr[10]) // panic: runtime error: ndex out of range [10] with length 1

类型断言失败

var i interface{} = "string"
_ = i.(int) // panic: interface conversion: interface {} is string, not int

关闭已关闭的channel

ch := make(chan int)
close(ch)
close(ch) // panic: close of closed channel

不可恢复的致命错误

并发读写Map

func TestPanic(t *testing.T) {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("Recovered:", r)
		}
	}()
	m := make(map[int]int)
	go func() {
		for {
			m[1] = 1
		}
	}()
	go func() {
		for {
			_ = m[1]
		}
	}()
    select {}
}
// fatal error: concurrent map read and map write

栈内存耗尽

func TestPanic(t *testing.T) {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("Recovered:", r)
		}
	}()
	var infiniteRecursion func()
	infiniteRecursion = func() {
		infiniteRecursion()
	}
	infiniteRecursion()
}
// runtime: goroutine stack exceeds 1000000000-byte limit
// runtime: sp=0x140201603a0 stack=[0x14020160000, 0x14040160000]
// fatal error: stack overflow

所有 Goroutine 死锁

func TestPanic(t *testing.T) {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("Recovered:", r)
		}
	}()
	var wg sync.WaitGroup
	ch := make(chan int) // 无缓冲通道

	wg.Add(1)
	go func() {
		defer wg.Done()
		<-ch // 阻塞等待数据
	}()

	wg.Wait() // 主goroutine等待
	ch <- 1   // 永远执行不到这里
}
// fatal error: all goroutines are asleep - deadlock!

内存分配失败(OOM)

程序主动退出

func TestPanic(t *testing.T) {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("Recovered:", r)
		}
	}()
	os.Exit(1) 
}
// 立即终止,不触发 recover

Echo框架的Recover中间件

实现原理其实很简单,就是在每个处理http请求的goroutine中使用recover函数handle期间的panic,并进行自定义的日志输出和http response结果返回。

// RecoverWithConfig returns a Recover middleware with config.
// See: `Recover()`.
func RecoverWithConfig(config RecoverConfig) echo.MiddlewareFunc {
	// Defaults
	if config.Skipper == nil {
		config.Skipper = DefaultRecoverConfig.Skipper
	}
	if config.StackSize == 0 {
		config.StackSize = DefaultRecoverConfig.StackSize
	}

	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			if config.Skipper(c) {
				return next(c)
			}

			defer func() {
				if r := recover(); r != nil {
					err, ok := r.(error)
					if !ok {
						err = fmt.Errorf("%v", r)
					}
					stack := make([]byte, config.StackSize)
					length := runtime.Stack(stack, !config.DisableStackAll)
					if !config.DisablePrintStack {
						c.Logger().Printf("[PANIC RECOVER] %v %s\n", err, stack[:length])
					}
					c.Error(err)
				}
			}()
			return next(c)
		}
	}
}

但有一点需要注意的是:中间件仅能捕获当前HTTP请求的Goroutine中可恢复的panic,后台Goroutine的panic或致命错误无法被中间件处理。