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

基于Golang的Clean架构:存储层错误处理优化方案问询

解决方案:基于Clean架构的错误分层处理

核心思路是在仓储层将底层数据库错误转换为领域层定义的抽象错误,上层(用例、控制器)仅依赖领域错误,完全隔离对GORM等底层框架的依赖。

1. 定义领域层抽象错误

在领域层(如domain/errors.go)定义业务相关的抽象错误类型,避免上层感知底层实现细节:

package domain

type DomainError struct {
    Code    string // 错误编码,用于上层判断
    Message string // 用户可见的错误信息
}

func (e *DomainError) Error() string {
    return e.Message
}

// 预定义业务场景常用错误
var (
    ErrUserNotFound = &DomainError{Code: "USER_NOT_FOUND", Message: "用户不存在"}
    ErrDatabaseFail = &DomainError{Code: "DB_FAILURE", Message: "数据库操作失败"}
)

2. 仓储层转换底层错误

仓储层作为框架驱动层,负责将GORM的具体错误转换为领域层的抽象错误:

package repository

import (
    "errors"
    "your-project/domain"
    "gorm.io/gorm"
)

type UserRepository struct {
    db *gorm.DB
}

func (r *UserRepository) GetUserByEmail(email string) (*domain.User, error) {
    var user domain.User
    result := r.db.Where("email = ?", email).First(&user)
    
    if result.Error != nil {
        // 匹配GORM的记录不存在错误,转换为领域错误
        if errors.Is(result.Error, gorm.ErrRecordNotFound) {
            return nil, domain.ErrUserNotFound
        }
        // 其他数据库错误统一转换为通用领域错误
        return nil, domain.ErrDatabaseFail
    }
    
    return &user, nil
}

3. 用例层处理业务错误映射

用例层根据业务需求,将领域错误转换为适合对外返回的错误(比如把用户不存在和密码错误统一返回"Invalid Params",防止枚举攻击):

package usecase

import (
    "errors"
    "your-project/domain"
    "golang.org/x/crypto/bcrypt"
)

type AuthUseCase struct {
    userRepo domain.UserRepository // 依赖领域层抽象接口
}

type UseCaseError struct {
    Code    string
    Message string
}

func (e *UseCaseError) Error() string {
    return e.Message
}

func (uc *AuthUseCase) Login(email, password string) (*domain.AuthToken, error) {
    user, err := uc.userRepo.GetUserByEmail(email)
    if err != nil {
        if errors.Is(err, domain.ErrUserNotFound) {
            // 业务层面统一返回参数错误,避免暴露具体原因
            return nil, &UseCaseError{Code: "INVALID_PARAMS", Message: "Invalid Params"}
        }
        // 其他领域错误直接传递
        return nil, err
    }

    // 校验密码
    if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
        return nil, &UseCaseError{Code: "INVALID_PARAMS", Message: "Invalid Params"}
    }

    // 生成认证Token逻辑...
    return &domain.AuthToken{Token: "xxx"}, nil
}

4. 控制器层处理用例错误

控制器仅依赖用例层的错误,无需感知GORM或仓储层细节:

package handler

import (
    "encoding/json"
    "errors"
    "net/http"
    "your-project/usecase"
)

type AuthHandler struct {
    authUseCase *usecase.AuthUseCase
}

func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
    var req struct {
        Email    string `json:"email"`
        Password string `json:"password"`
    }

    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        h.sendResponse(w, http.StatusBadRequest, "Invalid Request")
        return
    }

    token, err := h.authUseCase.Login(req.Email, req.Password)
    if err != nil {
        var ucErr *usecase.UseCaseError
        if errors.As(err, &ucErr) {
            if ucErr.Code == "INVALID_PARAMS" {
                h.sendResponse(w, http.StatusBadRequest, ucErr.Message)
                return
            }
        }
        // 未匹配到的错误统一返回500
        h.sendResponse(w, http.StatusInternalServerError, "Internal Server Error")
        return
    }

    h.sendResponse(w, http.StatusOK, token)
}

func (h *AuthHandler) sendResponse(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

5. Mock仓储层的便捷性

因为仓储层返回的是领域层预定义的错误,Mock时直接返回对应错误即可,无需处理GORM细节:

package usecase_test

import (
    "errors"
    "testing"
    "your-project/domain"
    "your-project/usecase"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
)

type MockUserRepo struct {
    mock.Mock
}

func (m *MockUserRepo) GetUserByEmail(email string) (*domain.User, error) {
    args := m.Called(email)
    return args.Get(0).(*domain.User), args.Error(1)
}

func TestLogin_UserNotFound(t *testing.T) {
    mockRepo := new(MockUserRepo)
    mockRepo.On("GetUserByEmail", "test@example.com").Return(nil, domain.ErrUserNotFound)

    uc := usecase.NewAuthUseCase(mockRepo)
    _, err := uc.Login("test@example.com", "123456")

    var ucErr *usecase.UseCaseError
    assert.True(t, errors.As(err, &ucErr))
    assert.Equal(t, "INVALID_PARAMS", ucErr.Code)
}

关键优势

  • 严格遵守Clean架构分层:上层完全不依赖底层框架(GORM),仅依赖领域抽象
  • 错误逻辑集中管理:仓储层统一转换底层错误,用例层统一映射业务错误
  • Mock成本低:无需模拟GORM的错误,直接返回领域预定义错误即可

内容的提问来源于stack exchange,提问作者Placeblock

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 11:27:43