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

项目结构
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
}
仓储层(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
}
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)
}
关键原则
- 依赖指向内部 — 领域层不依赖数据库或 HTTP
- 边界处使用接口 — 层之间使用接口以提高可测试性
- 处处传递 Context — 传递
context.Context以支持取消 - 错误作为值 — 定义领域错误,使用
errors.Is