AgentSkillsCN

go-clean-architecture-patterns

在 Go 后端中实施 Clean Architecture 模式的指南。适用于为移动应用新增功能、创建处理器、服务或仓库时使用。

SKILL.md
--- frontmatter
name: go-clean-architecture-patterns
description: Guide for implementing Clean Architecture patterns in the Go backend. Use this when adding new features, creating handlers, services, or repositories.

Go Clean Architecture Implementation

Architecture Layers

The backend follows Clean Architecture with 4 distinct layers:

code
internal/
├── api/                    # HTTP Layer (handlers, middleware, DTOs)
├── service/                # Business Logic Layer
├── repository/postgres/    # Data Access Layer
└── domain/                 # Domain Entities & Interfaces

Dependency Flow: Handler → Service → Repository → Database Rule: Inner layers never depend on outer layers

Adding a New Feature (Step-by-Step)

Example: Adding Product Reviews

1. Create Domain Entity (internal/domain/review.go)

go
package domain

import "time"

type Review struct {
    ID        int       `json:"id"`
    UserID    int       `json:"user_id"`
    ProductID int       `json:"product_id"`
    Rating    int       `json:"rating"`
    Comment   string    `json:"comment"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

// Repository interface (depends on domain, not implementation)
type ReviewRepository interface {
    Create(review *Review) error
    GetByID(id int) (*Review, error)
    GetByProductID(productID int, limit, offset int) ([]*Review, error)
    Update(review *Review) error
    Delete(id int) error
}

2. Create Repository Implementation (internal/repository/postgres/review_repo.go)

go
package postgres

import (
    "database/sql"
    "demo-project/ecommerce-backend/internal/domain"
)

type reviewRepository struct {
    db *sql.DB
}

func NewReviewRepository(db *sql.DB) domain.ReviewRepository {
    return &reviewRepository{db: db}
}

func (r *reviewRepository) Create(review *domain.Review) error {
    query := `
        INSERT INTO reviews (user_id, product_id, rating, comment)
        VALUES ($1, $2, $3, $4)
        RETURNING id, created_at, updated_at
    `
    return r.db.QueryRow(
        query,
        review.UserID,
        review.ProductID,
        review.Rating,
        review.Comment,
    ).Scan(&review.ID, &review.CreatedAt, &review.UpdatedAt)
}

// Implement other methods...

3. Create DTOs (internal/api/dto/review_dto.go)

go
package dto

type CreateReviewRequest struct {
    ProductID int    `json:"product_id" validate:"required,min=1"`
    Rating    int    `json:"rating" validate:"required,min=1,max=5"`
    Comment   string `json:"comment" validate:"max=1000"`
}

type ReviewResponse struct {
    ID        int    `json:"id"`
    UserID    int    `json:"user_id"`
    ProductID int    `json:"product_id"`
    Rating    int    `json:"rating"`
    Comment   string `json:"comment"`
    UserName  string `json:"user_name,omitempty"`
    CreatedAt string `json:"created_at"`
}

4. Create Service (internal/service/review_service.go)

go
package service

import (
    "demo-project/ecommerce-backend/internal/domain"
    "demo-project/ecommerce-backend/internal/api/dto"
)

type ReviewService struct {
    reviewRepo  domain.ReviewRepository
    userRepo    domain.UserRepository
    productRepo domain.ProductRepository
}

func NewReviewService(
    reviewRepo domain.ReviewRepository,
    userRepo domain.UserRepository,
    productRepo domain.ProductRepository,
) *ReviewService {
    return &ReviewService{
        reviewRepo:  reviewRepo,
        userRepo:    userRepo,
        productRepo: productRepo,
    }
}

func (s *ReviewService) CreateReview(userID int, req *dto.CreateReviewRequest) (*domain.Review, error) {
    // 1. Validate product exists
    _, err := s.productRepo.GetByID(req.ProductID)
    if err != nil {
        return nil, domain.ErrNotFound
    }

    // 2. Check user hasn't already reviewed
    // ... business logic ...

    // 3. Create review
    review := &domain.Review{
        UserID:    userID,
        ProductID: req.ProductID,
        Rating:    req.Rating,
        Comment:   req.Comment,
    }

    if err := s.reviewRepo.Create(review); err != nil {
        return nil, err
    }

    return review, nil
}

5. Create Handler (internal/api/handlers/review_handler.go)

go
package handlers

import (
    "net/http"
    "strconv"
    
    "github.com/gin-gonic/gin"
    
    "demo-project/ecommerce-backend/internal/api/dto"
    "demo-project/ecommerce-backend/internal/service"
    "demo-project/ecommerce-backend/pkg/response"
)

type ReviewHandler struct {
    reviewService *service.ReviewService
}

func NewReviewHandler(reviewService *service.ReviewService) *ReviewHandler {
    return &ReviewHandler{
        reviewService: reviewService,
    }
}

// @Summary Create a review
// @Tags reviews
// @Accept json
// @Produce json
// @Param review body dto.CreateReviewRequest true "Review data"
// @Success 201 {object} dto.ReviewResponse
// @Router /reviews [post]
// @Security BearerAuth
func (h *ReviewHandler) CreateReview(c *gin.Context) {
    var req dto.CreateReviewRequest
    
    if err := c.ShouldBindJSON(&req); err != nil {
        response.Error(c, http.StatusBadRequest, "Invalid request", err)
        return
    }

    // Get user ID from auth middleware
    userID := c.GetInt("user_id")

    review, err := h.reviewService.CreateReview(userID, &req)
    if err != nil {
        response.Error(c, http.StatusInternalServerError, "Failed to create review", err)
        return
    }

    response.Success(c, http.StatusCreated, "Review created", review)
}

6. Register Routes (internal/api/routes.go)

go
func SetupRoutes(router *gin.Engine, handlers *Handlers) {
    api := router.Group("/api/v1")
    
    // ... existing routes ...
    
    // Review routes
    reviews := api.Group("/reviews")
    reviews.Use(middleware.AuthMiddleware())
    {
        reviews.POST("", handlers.ReviewHandler.CreateReview)
        reviews.GET("/product/:id", handlers.ReviewHandler.GetProductReviews)
    }
}

7. Wire Dependencies (cmd/api/main.go)

go
func main() {
    // ... existing setup ...
    
    // Repositories
    userRepo := postgres.NewUserRepository(db)
    reviewRepo := postgres.NewReviewRepository(db) // Add this
    
    // Services
    authService := service.NewAuthService(userRepo)
    reviewService := service.NewReviewService(reviewRepo, userRepo, productRepo) // Add this
    
    // Handlers
    authHandler := handlers.NewAuthHandler(authService)
    reviewHandler := handlers.NewReviewHandler(reviewService) // Add this
    
    handlers := &api.Handlers{
        AuthHandler:   authHandler,
        ReviewHandler: reviewHandler, // Add this
    }
    
    // Setup routes
    routes.SetupRoutes(router, handlers)
}

Key Principles

1. Repository Layer

  • Only handles database operations
  • Returns domain entities, not DTOs
  • Uses SQL queries or ORM
  • Implements interfaces defined in domain
  • No business logic

2. Service Layer

  • Contains all business logic
  • Orchestrates between repositories
  • Performs validation
  • Handles transactions
  • Returns domain entities

3. Handler Layer

  • Handles HTTP concerns
  • Validates request DTOs
  • Calls service methods
  • Formats responses using pkg/response
  • Handles errors appropriately

4. Domain Layer

  • Pure domain entities
  • Interface definitions
  • Business errors
  • No dependencies on other layers

Common Patterns

Error Handling

go
// Define domain errors in internal/domain/errors.go
var (
    ErrNotFound      = errors.New("resource not found")
    ErrUnauthorized  = errors.New("unauthorized")
    ErrInvalidInput  = errors.New("invalid input")
)

// In service
if user == nil {
    return nil, domain.ErrNotFound
}

// In handler
if errors.Is(err, domain.ErrNotFound) {
    response.Error(c, http.StatusNotFound, "Not found", err)
    return
}

Validation

go
// Use validator tags in DTOs
type CreateRequest struct {
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"password" validate:"required,min=8"`
}

// Validate in handler
if err := validator.Validate(req); err != nil {
    response.Error(c, http.StatusBadRequest, "Validation failed", err)
    return
}

Standardized Responses

go
// Use pkg/response helpers
response.Success(c, http.StatusOK, "Success", data)
response.Error(c, http.StatusBadRequest, "Error message", err)

Middleware

go
// Create in internal/api/middleware/
// Apply in routes.go

Testing

Repository Tests

go
// Use test database
// Test SQL queries
// Mock database if needed

Service Tests

go
// Mock repository interfaces
// Test business logic
// Verify error handling

Handler Tests

go
// Use httptest
// Mock services
// Test HTTP responses

Checklist for New Features

  • Create domain entity in internal/domain/
  • Define repository interface in domain
  • Implement repository in internal/repository/postgres/
  • Create DTOs in internal/api/dto/
  • Implement service in internal/service/
  • Create handler in internal/api/handlers/
  • Register routes in internal/api/routes.go
  • Wire dependencies in cmd/api/main.go
  • Create database migration
  • Add Swagger comments
  • Write tests
  • Update API documentation