错误处理
概述
Go 语言中的错误信息可能较为简略,这使得准确查明问题根源变得困难。当错误在生产环境中通过错误追踪服务上报时,情况更是如此。
框架在 goyave.dev/goyave/v5/util/errors
包中提供了便捷的错误包装机制,具有以下优势:
- 调用栈在错误发生时立即收集,可精确定位所有错误的准确起源。
- 可以将多个错误包装成一个单一错误。
- 可以使用任何错误原因,无论是
string
、struct
、其他error
、map
、[]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()
来为您的错误追踪器提供所需的所有信息。