正在加载,请稍候…

使用整洁架构构建 Go REST API:Gin、GORM 与依赖注入

学习如何使用整洁架构构建可维护的 Go REST API,涵盖 handler/service/repository 分层、GORM、Wire 依赖注入、中间件

Go REST API with Clean Architecture: Gin, GORM, and Dependency Injection

Go 中的整洁架构

整洁架构将关注点分离到不同层,使 Go API 更易于维护和测试。

Go REST API with Clean Architecture: Gin, GORM, and Dependency Injection illustration

项目结构

myapi/
├── cmd/
│   └── server/
│       └── main.go
├── internal/
│   ├── domain/         # 实体、接口
│   │   ├── user.go
│   │   └── repository.go
│   ├── usecase/        # 业务逻辑
│   │   └── user_usecase.go
│   ├── repository/     # 数据访问
│   │   └── user_repository.go
│   ├── delivery/       # HTTP 处理器
│   │   └── http/
│   │       ├── handler.go
│   │       └── middleware.go
│   └── infrastructure/ # 数据库、缓存设置
│       └── database.go
├── pkg/
│   ├── logger/
│   └── validator/
├── go.mod
└── go.sum

领域层

// internal/domain/user.go
package domain

import (
    "time"
    "context"
)

type User struct {
    ID        uint      `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

type UserRepository interface {
    FindByID(ctx context.Context, id uint) (*User, error)
    FindAll(ctx context.Context, page, size int) ([]*User, error)
    Create(ctx context.Context, user *User) error
    Update(ctx context.Context, user *User) error
    Delete(ctx context.Context, id uint) error
    ExistsByEmail(ctx context.Context, email string) (bool, error)
}

type UserUseCase interface {
    GetUser(ctx context.Context, id uint) (*User, error)
    ListUsers(ctx context.Context, page, size int) ([]*User, error)
    CreateUser(ctx context.Context, input CreateUserInput) (*User, error)
    UpdateUser(ctx context.Context, id uint, input UpdateUserInput) (*User, error)
    DeleteUser(ctx context.Context, id uint) error
}

type CreateUserInput struct {
    Name  string
    Email string
}

Go REST API with Clean Architecture: Gin, GORM, and Dependency Injection illustration

仓储层(GORM)

// internal/repository/user_repository.go
package repository

import (
    "context"
    "errors"
    "myapi/internal/domain"
    "gorm.io/gorm"
)

type userRepository struct {
    db *gorm.DB
}

func NewUserRepository(db *gorm.DB) domain.UserRepository {
    return &userRepository{db: db}
}

func (r *userRepository) FindByID(ctx context.Context, id uint) (*domain.User, error) {
    var user domain.User
    result := r.db.WithContext(ctx).First(&user, id)
    if errors.Is(result.Error, gorm.ErrRecordNotFound) {
        return nil, domain.ErrNotFound
    }
    return &user, result.Error
}

func (r *userRepository) FindAll(ctx context.Context, page, size int) ([]*domain.User, error) {
    var users []*domain.User
    offset := (page - 1) * size
    result := r.db.WithContext(ctx).
        Offset(offset).
        Limit(size).
        Order("created_at DESC").
        Find(&users)
    return users, result.Error
}

func (r *userRepository) Create(ctx context.Context, user *domain.User) error {
    return r.db.WithContext(ctx).Create(user).Error
}

用例层

// internal/usecase/user_usecase.go
package usecase

import (
    "context"
    "myapi/internal/domain"
)

type userUseCase struct {
    userRepo domain.UserRepository
}

func NewUserUseCase(repo domain.UserRepository) domain.UserUseCase {
    return &userUseCase{userRepo: repo}
}

func (uc *userUseCase) CreateUser(ctx context.Context, input domain.CreateUserInput) (*domain.User, error) {
    exists, err := uc.userRepo.ExistsByEmail(ctx, input.Email)
    if err != nil {
        return nil, err
    }
    if exists {
        return nil, domain.ErrEmailTaken
    }
    
    user := &domain.User{
        Name:  input.Name,
        Email: input.Email,
    }
    
    if err := uc.userRepo.Create(ctx, user); err != nil {
        return nil, err
    }
    
    return user, nil
}

Go REST API with Clean Architecture: Gin, GORM, and Dependency Injection illustration

HTTP 处理器(Gin)

// internal/delivery/http/handler.go
package http

import (
    "net/http"
    "strconv"
    "myapi/internal/domain"
    "github.com/gin-gonic/gin"
)

type UserHandler struct {
    userUC domain.UserUseCase
}

func (h *UserHandler) GetUser(c *gin.Context) {
    id, err := strconv.ParseUint(c.Param("id"), 10, 32)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
        return
    }
    
    user, err := h.userUC.GetUser(c.Request.Context(), uint(id))
    if err != nil {
        switch err {
        case domain.ErrNotFound:
            c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
        default:
            c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
        }
        return
    }
    
    c.JSON(http.StatusOK, user)
}

func (h *UserHandler) CreateUser(c *gin.Context) {
    var input domain.CreateUserInput
    if err := c.ShouldBindJSON(&input); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    
    user, err := h.userUC.CreateUser(c.Request.Context(), input)
    if err != nil {
        c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
        return
    }
    
    c.JSON(http.StatusCreated, user)
}

中间件

// JWT auth middleware
func JWTMiddleware(secret string) gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization")
        if token == "" {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing token"})
            return
        }
        
        claims, err := validateJWT(token, secret)
        if err != nil {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
            return
        }
        
        c.Set("user_id", claims.UserID)
        c.Next()
    }
}

// Request logger middleware
func LoggerMiddleware(logger *zap.Logger) gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        c.Next()
        
        logger.Info("request",
            zap.String("method", c.Request.Method),
            zap.String("path", c.Request.URL.Path),
            zap.Int("status", c.Writer.Status()),
            zap.Duration("latency", time.Since(start)),
        )
    }
}

测试

func TestCreateUser(t *testing.T) {
    // Mock repository
    mockRepo := &MockUserRepository{}
    mockRepo.On("ExistsByEmail", mock.Anything, "john@example.com").Return(false, nil)
    mockRepo.On("Create", mock.Anything, mock.AnythingOfType("*domain.User")).Return(nil)
    
    uc := NewUserUseCase(mockRepo)
    
    user, err := uc.CreateUser(context.Background(), domain.CreateUserInput{
        Name:  "John Doe",
        Email: "john@example.com",
    })
    
    assert.NoError(t, err)
    assert.Equal(t, "John Doe", user.Name)
    mockRepo.AssertExpectations(t)
}

关键原则

  1. 依赖指向内部 — 领域层不依赖数据库或 HTTP
  2. 边界处使用接口 — 层之间使用接口以提高可测试性
  3. 处处传递 Context — 传递 context.Context 以支持取消
  4. 错误作为值 — 定义领域错误,使用 errors.Is