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+ 端点、多人协作时,清晰的层级边界是保持代码可维护性的关键。