Skip to content

错误处理

概述

Go 语言中的错误信息可能较为简略,这使得准确查明问题根源变得困难。当错误在生产环境中通过错误追踪服务上报时,情况更是如此。

框架在 goyave.dev/goyave/v5/util/errors 包中提供了便捷的错误包装机制,具有以下优势:

  • 调用栈在错误发生时立即收集,可精确定位所有错误的准确起源。
  • 可以将多个错误包装成一个单一错误。
  • 可以使用任何错误原因,无论是 stringstruct、其他 errormap[]error[]any 等。
  • 可以附加更多信息到错误中,这对于错误报告服务非常有用。
  • 包装器由结构化日志记录器处理,从而生成更详细的日志。错误原因会自动转换为 slog 属性。
    • 在开发模式下,错误原因和堆栈跟踪会以易于人类阅读的格式显示。
    • 在生产环境中,错误原因和堆栈跟踪会被序列化为 JSON 并添加到日志条目中。
    • trace 属性将包含堆栈帧。
    • message 将包含 Error() 函数的结果,即所有包装错误的消息用换行符 (\n) 连接而成。
    • 如果错误原因是实现了 slog.LogValuer 的自定义错误类型,则会添加一个 reason 属性,其中包含 LogValue() 返回的值。
  • 包装器支持嵌套错误:可以包装已经包装了其他错误的错误。

下图解释了整体错误包装流程:

错误包装示意图

指南

  • 不鼓励使用 panic,但在某些情况下仍可使用。
  • 任何返回的错误都应使用 errors.New() 进行包装
    • 如果您正在开发库,可以使用 errors.NewSkip() 跳过前几个堆栈帧,并返回指向库函数调用而非库内部代码的堆栈跟踪。
  • 尽可能将错误向上传递,通常传递到控制器处理程序。
  • 使用 response.Error() 报告错误。这将记录错误并将响应状态设置为 http.StatusInternalServerError。在调试模式下,错误将写入响应体;否则,将执行相应的状态处理器
go
func (ctrl *Controller) Show(response *goyave.Response, request *goyave.Request) {
	err := ctrl.Service.SomeProcess()
	if err != nil {
		response.Error(err)
		return
	}
	//...
}

在以下示例中,我们调用 HighLevelFunc() 并期望 someProcess() 返回一个错误。因为我们一返回错误就将其包装,堆栈跟踪将精确指向错误的起源位置。

go
import (
	//...
	"goyave.dev/goyave/v5/util/errors"
)

func HighLevelFunc() error {
	err := SomeFunc()
	if err != nil {
		return errors.New(err)
	}
	//...
	return nil
}

func SomeFunc() error {
	value, err := someProcess()
	if err != nil {
		return errors.New(err) // 跟踪将指向这里
	}
	//...
	return nil
}

INFO

  • errors.New() 接收到一个已经被包装的原因(*errors.Error)时,该原因会被原样返回。因此跟踪信息不会被修改,也不会丢失任何信息。
  • 然而,始终使用包装器是一个好习惯,这样可以确保您的错误在某个时刻总是被包装。
  • errors.New(nil) 返回 nil。如果原因是 []error[]any,则 nil 元素会被忽略。
  • *errors.Error.Error() 仅返回错误消息而不包含堆栈跟踪。如果您在框架的 slogger 上下文之外使用此类型,建议使用 *errors.Error.String()

恢复中间件

Goyave 有一个内置的全局中间件,确保所有未恢复的 panic 都能被优雅地恢复。当此中间件从 panic 中恢复时,如果尚未包装,它会将其包装,然后记录它。最后,响应状态被设置为 http.StatusInternalServerError,并执行此代码对应的状态处理器

此机制确保了应用程序的韧性,因为总是会向客户端发送正确的响应。此外,借助与常规包装错误相同的精确性,它有助于解决意外的 panic。

状态处理器

请求生命周期内部产生错误时,无论是被恢复中间件恢复的 panic,还是使用 response.Error() 报告的错误,都会执行与 500 状态码关联的状态处理器

默认的错误状态处理器可以用您自己的处理器替换,让您能够以集中方式处理错误。如果您想将错误报告给错误追踪服务,这会很有帮助。

go
// http/controller/status/status.go
type PanicStatusHandler struct {
	goyave.Component
}

func (*PanicStatusHandler) Handle(response *goyave.Response, _ *goyave.Request) {
	errortracker.Notify(response.GetError())

	message := map[string]string{
		"error": http.StatusText(response.GetStatus()),
	}
	response.JSON(response.GetStatus(), message)
}

WARNING

  • 不要忘记 *errors.Error 可能包装了多个错误。为确保所有包装的错误都能在错误追踪器中正确报告,请使用 err.Len()err.Unwrap()
  • 尽管 *errors.Error 实现了错误报告服务常用的方法,例如 Callers() []uintptr,并且应该能与许多此类服务开箱即用,但您使用的服务可能不支持。请使用 err.FileLine(), err.StackFrames(), err.Callers() 来为您的错误追踪器提供所需的所有信息。