在Go语言开发中,错误处理不是简单的返回err就结束。一个可维护的错误结构应当让错误具备可读、可判别、可追踪的能力,使系统在迭代中仍能保持清晰的故障定位路径。
为什么需要自定义错误结构
标准库中的error接口只有一个Error() string方法,仅能表达一段文字。当服务被拆分为多个包与层级后,上游只能靠字符串匹配来判断错误类型,这种做法极其脆弱。例如数据库层返回“connection refused”,业务层若用字符串包含来判断,一旦底层修改文案就会失效。
自定义错误结构可以把错误码、发生层级、原始错误、时间戳等字段封装起来。这样中间件能根据错误码决定HTTP状态码,日志组件能提取结构化字段,测试也能用类型断言验证行为。下面定义一个基础错误类型:
type AppError struct {
Code string // 业务错误码,如 USER_NOT_FOUND
Layer string // 发生层,如 repository/service
Message string // 对用户或调用方可读的信息
Err error // 被包装的原始错误
}
func (e *AppError) Error() string {
if e.Err != nil {
return e.Layer + ": " + e.Message + " -> " + e.Err.Error()
}
return e.Layer + ": " + e.Message
}
func (e *AppError) Unwrap() error {
return e.Err
}
使用错误包装保持链路完整
Go 1.13之后,errors包提供了Wrap思路的官方支持。通过fmt.Errorf配合%w动词,可以构建错误链,同时保留底层错误。调用方使用errors.Is判断特定错误,用errors.As提取自定义类型,而不依赖文本。
以下示例展示在service层包装repository层的错误,并附加上下文:
import (
"errors"
"fmt"
)
var ErrUserNotFound = errors.New("user not found")
func (r *UserRepo) Find(id int) (*User, error) {
u, err := r.db.Query(id)
if err != nil {
return nil, fmt.Errorf("query db: %w", err)
}
if u == nil {
return nil, ErrUserNotFound
}
return u, nil
}
func (s *UserService) GetProfile(id int) (*Profile, error) {
u, err := s.repo.Find(id)
if err != nil {
var appErr *AppError
if errors.As(err, &appErr) {
return nil, appErr
}
return nil, &AppError{
Code: "SERVICE_USER_GET",
Layer: "service",
Message: "failed to get user profile",
Err: err,
}
}
return &Profile{Name: u.Name}, nil
}
上述代码中,如果repository已经返回了AppError,service层直接透传;否则构造新的AppError并包装底层err。因为实现了Unwrap,errors.As仍能向下找到原始错误类型,实现跨层识别。
区分业务错误与系统错误
并非所有错误都需要完整堆栈。业务错误(如参数校验失败)属于预期流程,不应打印堆栈;系统错误(如网络中断)才需要详细追踪。可以在AppError中增加一个Kind字段:
type ErrorKind int
const (
KindBusiness ErrorKind = iota
KindSystem
)
type AppError struct {
Code string
Layer string
Message string
Err error
Kind ErrorKind
}
在HTTP中间件中,根据Kind决定日志级别与响应体。业务错误返回200或400并带Code,系统错误返回500且不暴露内部信息。这种分类降低了噪声,也提升了可维护性。
| 错误类型 | 是否记录堆栈 | HTTP状态码建议 |
|---|---|---|
| KindBusiness | 否 | 400/422 |
| KindSystem | 是 | 500 |
避免常见设计误区
一个误区是把所有错误都转成同一个字符串然后返回,导致错误链断裂。另一个误区是在每个函数里都包装并添加冗长描述,造成性能浪费与日志膨胀。应当只在边界处(如入口层、外部调用后)做包装,内部函数直接返回原始错误即可。
此外,不要滥用panic处理可预期错误。panic只应用于真正不可恢复的程序缺陷,如配置加载失败。业务可预期失败请用error结构传递,保证调用方有能力处理。
可维护的错误结构目标:让错误自己说话,让调用方低成本判别,让系统低耦合追踪。
小结与实践建议
设计可维护错误结构的核心步骤:定义携带码与层的自定义类型;在边界使用%w包装;用errors.Is和As代替字符串判断;区分业务与系统错误。团队可统一一个errors包,提供NewBusiness、WrapSystem等构造函数,减少重复代码。
当项目引入OpenTelemetry等追踪工具时,可将AppError的Code作为span属性,实现错误与链路的联动。长远看,良好的错误结构比写更多测试用例更能提升系统的可观测性。
Golangerror_handlingerror_wrapping修改时间:2026-08-01 13:18:33