2. Go 项目结构基础:标准布局与模块化设计

在构建业务模块与领域边界之前,必须先建立一套稳定、可扩展的项目骨架。Go 社区经过多年实践,形成了以 cmd/internal/pkg 为核心的标准项目布局,配合模块化设计原则,能够有效支撑 DDD 战术设计落地。本章将拆解这套骨架的职责边界与设计规则。

2.1 标准 Go 项目布局总览

标准布局并非官方强制规范,而是社区共识的最佳实践。它通过目录命名与层级关系,清晰表达了“入口-内部实现-可复用库”的三层结构。下图展示了最常用的布局形态:

标准 Go 项目布局核心结构 项目根目录 (your-project/) cmd/ 可执行程序入口 cmd/api cmd/worker cmd/cli cmd/migrate internal/ 私有应用逻辑(禁止外部导入) internal/user internal/order internal/handler internal/service pkg/ 可复用公共库(对外暴露) pkg/validator pkg/logger pkg/middleware pkg/errors 其他辅助目录 configs/ scripts/ deployments/ docs/
关键原则:cmd 目录下的每个子目录都是一个独立的 main 包,只负责组装依赖、启动进程;internal 目录受 Go 编译器保护,外部项目无法导入;pkg 目录存放与业务无关的通用工具,可被外部项目引用。

2.2 三大核心目录职责对比

目录 核心职责 导入限制 典型内容 与 DDD 的关系
cmd/ 可执行程序入口,依赖组装与启动 仅导入 internal 和 pkg main.go、依赖注入、配置加载、服务器启动 对应 DDD 的“基础设施层”启动点
internal/ 私有应用逻辑,领域核心 禁止外部项目导入 领域模型、应用服务、仓储接口、Handler 承载领域层与应用层核心代码
pkg/ 可复用公共库,与业务无关 无限制,对外暴露 日志、验证器、错误处理、HTTP 中间件 支撑基础设施层,不包含领域知识
常见误区:将业务逻辑直接写在 cmd/main.go 中,或将领域模型放在 pkg 目录下。cmd 应保持“薄入口”,pkg 不应包含任何业务语义。

2.3 cmd 目录:薄入口与依赖组装

cmd 目录下的每个子目录对应一个独立的二进制程序。每个 cmd 子目录只包含一个 main.go 文件,职责仅限于:

  • 读取配置(环境变量、配置文件)
  • 初始化基础设施连接(数据库、消息队列、缓存)
  • 组装依赖并启动应用服务
  • 注册信号处理实现优雅关闭
// cmd/api/main.go
package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "your-project/internal/handler"
    "your-project/internal/repository"
    "your-project/internal/service"
    "your-project/pkg/logger"
    "your-project/pkg/middleware"
)

func main() {
    // 1. 加载配置
    cfg := loadConfig()

    // 2. 初始化基础设施
    db := initDatabase(cfg.DSN)
    log := logger.New(cfg.LogLevel)

    // 3. 组装依赖
    userRepo := repository.NewUserRepo(db)
    userSvc := service.NewUserService(userRepo, log)
    userHandler := handler.NewUserHandler(userSvc)

    // 4. 注册路由
    mux := http.NewServeMux()
    mux.HandleFunc("POST /users", userHandler.CreateUser)
    mux.HandleFunc("GET /users/{id}", userHandler.GetUser)

    // 5. 启动 HTTP 服务器
    srv := &http.Server{
        Addr:    ":" + cfg.Port,
        Handler: middleware.Chain(mux, middleware.Recovery(log), middleware.RequestID),
    }

    // 6. 优雅关闭
    go func() {
        sigCh := make(chan os.Signal, 1)
        signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
        <-sigCh
        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()
        srv.Shutdown(ctx)
    }()

    log.Info("server starting", "port", cfg.Port)
    if err := srv.ListenAndServe(); err != http.ErrServerClosed {
        log.Fatal("server error", "error", err)
    }
}
设计要点:cmd 目录不包含任何业务逻辑判断。所有业务决策都委托给 internal 中的 service 层。如果多个 cmd 子程序需要共享启动逻辑,可以抽取到 internal/pkg/server 中,但不要放在 cmd 目录下。

2.4 internal 目录:领域核心的保护区

internal 目录是 Go 语言提供的编译级隔离机制。任何外部项目都无法导入 internal 包及其子包,这为领域模型提供了天然的封装边界。在 DDD 实践中,internal 通常按业务领域或架构层次组织:

internal 目录的两种组织模式 模式一:按领域组织(推荐 DDD) internal/user/ entity.go repository.go service.go handler.go internal/order/ entity.go service.go 模式二:按层次组织(传统分层) internal/handler/ internal/service/ internal/repository/ internal/model/ internal/middleware/ 对比
组织模式 优点 缺点 适用场景
按领域组织 高内聚,领域边界清晰,便于微服务拆分 跨领域调用需定义接口 DDD 项目、中大型业务系统
按层次组织 结构简单,团队熟悉度高 领域逻辑分散,不利于演进 小型项目、CRUD 为主的系统
推荐做法:对于 DDD 项目,优先采用“按领域组织”模式。每个领域包内部再按层次划分子包(如 entity、service、repository),实现“领域内分层,领域间隔离”。

2.5 pkg 目录:可复用公共库

pkg 目录存放与具体业务无关的通用组件。这些组件可以被当前项目和其他项目直接导入。典型内容包括:

  • 日志库:封装统一的日志接口,支持结构化输出
  • 验证器:通用的请求参数校验工具
  • 错误处理:自定义错误类型与错误码规范
  • HTTP 中间件:跨域、限流、鉴权、请求追踪
  • 数据库工具:连接池管理、事务辅助函数
// pkg/errors/errors.go
package errors

import "fmt"

type AppError struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
    Err     error  `json:"-"`
}

func (e *AppError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("code=%d, message=%s, err=%v", e.Code, e.Message, e.Err)
    }
    return fmt.Sprintf("code=%d, message=%s", e.Code, e.Message)
}

func New(code int, message string) *AppError {
    return &AppError{Code: code, Message: message}
}

func Wrap(code int, message string, err error) *AppError {
    return &AppError{Code: code, Message: message, Err: err}
}
重要约束:pkg 目录中的代码不能依赖 internal 目录中的任何包。如果某个工具函数需要引用领域模型,它应该放在 internal 中,而不是 pkg 中。pkg 是“无业务语义”的公共库。

2.6 模块化设计原则

Go 项目的模块化设计遵循以下核心原则,这些原则与 DDD 的限界上下文思想高度一致:

原则 说明 Go 项目落地方式
高内聚 一个包内的代码应围绕同一职责变化 每个 internal 子包只处理一个领域概念
低耦合 包之间通过接口依赖,而非具体实现 仓储接口定义在领域包,实现在 repository 子包
依赖反转 高层模块不依赖低层模块,双方依赖抽象 service 依赖 repository 接口,而非具体数据库实现
显式依赖 依赖通过构造函数显式注入 NewUserService(repo Repository, log Logger)
包隔离 internal 阻止外部意外依赖 编译器强制保护领域核心

2.7 模块化设计实战:用户注册流程

以下代码展示了模块化原则在用户注册场景中的完整应用:

// internal/user/entity.go — 领域实体
package user

import "your-project/pkg/errors"

type User struct {
    ID       string
    Email    string
    Password string
    Nickname string
}

// internal/user/repository.go — 仓储接口(依赖反转)
type Repository interface {
    Save(ctx context.Context, user *User) error
    FindByEmail(ctx context.Context, email string) (*User, error)
}

// internal/user/service.go — 应用服务
type Service struct {
    repo Repository
    log  Logger
}

func NewService(repo Repository, log Logger) *Service {
    return &Service{repo: repo, log: log}
}

func (s *Service) Register(ctx context.Context, email, password, nickname string) (*User, error) {
    // 1. 检查邮箱是否已注册
    existing, _ := s.repo.FindByEmail(ctx, email)
    if existing != nil {
        return nil, errors.New(409, "email already registered")
    }

    // 2. 创建用户实体
    user := &User{
        ID:       generateID(),
        Email:    email,
        Password: hashPassword(password),
        Nickname: nickname,
    }

    // 3. 持久化
    if err := s.repo.Save(ctx, user); err != nil {
        s.log.Error("failed to save user", "error", err)
        return nil, errors.Wrap(500, "internal server error", err)
    }

    return user, nil
}

// internal/user/repository/mysql.go — 仓储实现(基础设施层)
package repository

import (
    "context"
    "database/sql"
    "your-project/internal/user"
)

type MySQLUserRepo struct {
    db *sql.DB
}

func NewMySQLUserRepo(db *sql.DB) *MySQLUserRepo {
    return &MySQLUserRepo{db: db}
}

func (r *MySQLUserRepo) Save(ctx context.Context, u *user.User) error {
    _, err := r.db.ExecContext(ctx,
        "INSERT INTO users (id, email, password, nickname) VALUES (?, ?, ?, ?)",
        u.ID, u.Email, u.Password, u.Nickname)
    return err
}

func (r *MySQLUserRepo) FindByEmail(ctx context.Context, email string) (*user.User, error) {
    row := r.db.QueryRowContext(ctx, "SELECT id, email, password, nickname FROM users WHERE email = ?", email)
    u := &user.User{}
    err := row.Scan(&u.ID, &u.Email, &u.Password, &u.Nickname)
    if err == sql.ErrNoRows {
        return nil, nil
    }
    return u, err
}
模块化收益:当需要将 MySQL 替换为 PostgreSQL 时,只需新增一个 repository/postgres.go 实现 Repository 接口,领域层 service 无需任何修改。这就是依赖反转与接口隔离带来的可测试性和可扩展性。

2.8 本章小结

标准 Go 项目布局通过 cmd/internal/pkg 三层结构,为业务模块提供了清晰的物理边界:

  • cmd:薄入口,只做依赖组装与启动
  • internal:领域核心保护区,按业务或层次组织
  • pkg:可复用公共库,无业务语义

模块化设计原则(高内聚、低耦合、依赖反转、显式依赖)与 DDD 的限界上下文思想一脉相承,是后续章节中领域建模与边界设计的基石。下一章将深入探讨如何利用 Go 的接口机制实现领域层的依赖反转与仓储模式。