You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 06:30:04