ccy

Go 后端项目结构最佳实践:分层架构与依赖注入

·0 次阅读·
Go后端架构

为什么需要规范的项目结构

Go 语言在设计上保持了极简主义——没有泛型(1.18 之前)、没有继承、没有注解。这种简洁性是一把双刃剑:一方面降低了语言的学习成本,另一方面也意味着 Go 社区缺乏统一的项目结构约定。不同的项目有不同的组织方式,这给代码的可读性和可维护性带来了挑战。

经过多个项目的实践,我整理了一套适用于中小型 API 服务的项目结构规范。

目录结构

project-root/
├── cmd/
│   └── server/
│       └── main.go         # 应用入口,仅负责依赖组装和启动
├── internal/
│   ├── handler/            # HTTP Handler(传输层)
│   ├── service/            # 业务逻辑层
│   ├── repository/         # 数据访问层
│   ├── model/              # 领域模型定义
│   ├── middleware/         # HTTP 中间件
│   └── config/            # 配置解析
├── pkg/
│   ├── response/          # 统一响应格式
│   ├── validator/         # 自定义验证器
│   └── logger/            # 日志抽象
├── migrations/            # 数据库迁移文件
├── go.mod
└── go.sum

分层架构详解

Handler 层(传输层)

Handler 只做三件事:解析请求、调用 Service、返回响应。不应该包含任何业务逻辑。

func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
    var req CreateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        response.Error(w, http.StatusBadRequest, "invalid request body")
        return
    }

    user, err := h.svc.Create(r.Context(), req.ToDTO())
    if err != nil {
        response.Error(w, http.StatusInternalServerError, err.Error())
        return
    }

    response.JSON(w, http.StatusCreated, user)
}

Service 层(业务逻辑层)

Service 是核心业务逻辑的载体。它不关心 HTTP 细节(Request/Response),也不关心数据存储的底层实现。这种抽象使得业务逻辑可以独立于传输协议和数据源进行单元测试。

type UserService struct {
    repo UserRepository
    email EmailService
}

func (s *UserService) Create(ctx context.Context, dto CreateUserDTO) (*User, error) {
    if err := s.validateEmail(dto.Email); err != nil {
        return nil, fmt.Errorf("validate email: %w", err)
    }

    hash, err := bcrypt.GenerateFromPassword([]byte(dto.Password), bcrypt.DefaultCost)
    if err != nil {
        return nil, fmt.Errorf("hash password: %w", err)
    }

    user := &User{
        Email:    dto.Email,
        Password: string(hash),
        CreatedAt: time.Now(),
    }

    if err := s.repo.Create(ctx, user); err != nil {
        return nil, fmt.Errorf("create user: %w", err)
    }

    go s.email.SendWelcome(user.Email)
    return user, nil
}

Repository 层(数据访问层)

Repository 封装所有数据持久化操作。接口定义在 Service 层,实现放在 Repository 层,通过接口隔离实现依赖反转。

// Service 层定义接口
type UserRepository interface {
    Create(ctx context.Context, user *User) error
    FindByID(ctx context.Context, id int64) (*User, error)
    FindByEmail(ctx context.Context, email string) (*User, error)
}

// Repository 层的 Postgres 实现
type userRepo struct {
    db *sql.DB
}

func (r *userRepo) Create(ctx context.Context, user *User) error {
    query := `INSERT INTO users (email, password, created_at) VALUES ($1, $2, $3) RETURNING id`
    return r.db.QueryRowContext(ctx, query, user.Email, user.Password, user.CreatedAt).Scan(&user.ID)
}

依赖注入的实现方式

Go 的依赖注入不需要框架。通过 Constructor Injection(构造函数注入)即可实现:

func InitializeApp(cfg *config.Config) *http.Server {
    db := initDB(cfg.DSN)

    userRepo := repository.NewUserRepo(db)
    userSvc := service.NewUserService(userRepo, emailSvc)
    userHandler := handler.NewUserHandler(userSvc)

    router := initRouter(userHandler)
    return &http.Server{Addr: cfg.Addr, Handler: router}
}

这种手动组装的方式(也称为 Dependency Injection Without Framework)在中小型项目中表现出色:没有反射、没有代码生成、编译期即可发现类型错误。

当项目规模增长到 50+ 依赖时,可以考虑 Google Wire 这样的代码生成工具,但基本原则不变——依赖从外向内注入,确保每层都可以被独立 Mock 和测试。

中间件链的组织

中间件采用洋葱模型(Onion Model),请求依次经过:

Request → Logger → Recovery → CORS → Auth → Router → Handler

Go 1.22 新增的 http.ServeMux 支持路由模式匹配,对于简单的 API 服务已经足够,不需要引入 chi 或 gorilla/mux:

mux := http.NewServeMux()
mux.HandleFunc("GET /api/users/{id}", userHandler.GetByID)
mux.HandleFunc("POST /api/users", userHandler.Create)

// 中间件链
handler := middleware.Logger(middleware.Recovery(middleware.CORS(mux)))

总结

这套结构的核心思想是 分层解耦 + 接口隔离。Handler 不知道数据存在哪,Service 不知道请求从哪来,Repository 不知道数据怎么用。每一层只关心自己的职责,通过接口进行契约通信。

对于小项目(<10 个 API 端点),这种分层可能显得冗余。但当项目增长到 50+ 端点、多人协作时,清晰的层级边界是保持代码可维护性的关键。

评论

发表评论