Go语言中如何实现解耦式错误处理?分层错误解耦的惯用方式
应用层级间解耦错误信息的惯用方式
先看示例代码:
var ErrNotFound = errors.New("not found") type Resource struct { Name string } type repository interface { CreateResource(ctx context.Context, name string) (*Resource, error) } type Controller struct { Repo repository } func (c *Controller) CreateResource(ctx context.Context, name string) (*Resource, error) { r, err := c.Repo.CreateResource(ctx, name) // error checking ... if err != nil { } return r, nil }
你提到的「控制器定义仓库哨兵错误」的做法并不合理,下面是Go生态里应用层级间解耦错误的几种惯用方式:
1. 错误与接口契约绑定,归属抽象层
仓库接口是底层实现的行为契约,语义化的哨兵错误应该和接口放在同一个抽象包中(比如单独的repo包),而不是放在上层控制器里。
这样做的原因是:错误是接口契约的一部分——仓库实现需要返回哪些类型的错误,本身就是接口约定的内容。把错误和接口放在一起,既保证契约统一,也避免上层逻辑和底层错误产生不必要的耦合,同时符合依赖倒置原则(上层依赖下层抽象,而非下层依赖上层定义)。
2. 用错误包装传递上下文,上层只做语义判断
Go 1.13+支持通过fmt.Errorf("%w", err)包装错误,上层可以用errors.Is()/errors.As()判断底层错误的语义,不用关心错误的具体来源:
- 仓库层返回自己的专属错误(比如数据库层面的
ErrDuplicateEntry、ErrConnTimeout) - 控制器层不需要深究错误细节,只通过
errors.Is()识别预期的错误类型,再根据业务需求决定是否重新包装成更贴近上层的错误返回。
示例代码:
仓库实现:
// repo包内定义 var ErrNotFound = errors.New("resource not found") func (r *mysqlRepo) CreateResource(ctx context.Context, name string) (*Resource, error) { _, err := r.db.Exec("INSERT INTO ...", name) if err != nil { if strings.Contains(err.Error(), "no such table") { return nil, fmt.Errorf("%w", ErrNotFound) } return nil, err } return &Resource{Name: name}, nil }
控制器逻辑:
func (c *Controller) CreateResource(ctx context.Context, name string) (*Resource, error) { r, err := c.Repo.CreateResource(ctx, name) if err != nil { if errors.Is(err, repo.ErrNotFound) { // 包装成业务侧更易理解的错误 return nil, fmt.Errorf("failed to create resource: dependent data not found: %w", err) } return nil, err } return r, nil }
3. 自定义错误类型传递结构化信息
如果需要携带错误码、资源ID等额外上下文,可以自定义错误类型并实现error接口。上层通过errors.As()断言错误类型,既能解耦层级,又能获取必要的错误细节。
示例:
// repo包内定义自定义错误 type ResourceError struct { Code int ResourceID string Description string } func (e *ResourceError) Error() string { return fmt.Sprintf("resource error [code:%d, id:%s]: %s", e.Code, e.ResourceID, e.Description) } // 仓库返回自定义错误 func (r *mysqlRepo) CreateResource(ctx context.Context, name string) (*Resource, error) { // 模拟操作失败 return nil, &ResourceError{ Code: 409, ResourceID: name, Description: "resource name already exists", } } // 控制器处理 func (c *Controller) CreateResource(ctx context.Context, name string) (*Resource, error) { r, err := c.Repo.CreateResource(ctx, name) if err != nil { var resErr *repo.ResourceError if errors.As(err, &resErr) { // 根据错误码做不同业务处理 if resErr.Code == 409 { return nil, fmt.Errorf("resource name '%s' is already taken", resErr.ResourceID) } } return nil, err } return r, nil }
4. 避免上层定义底层错误契约
控制器属于业务逻辑上层,职责是协调仓库、处理业务规则,而非定义底层仓库的错误规范。如果把仓库的哨兵错误放在控制器层,会导致仓库实现必须依赖控制器包,形成反向依赖,破坏代码的分层结构。
内容的提问来源于stack exchange,提问作者Erik Johansson
相关产品推荐
相关产品推荐

