Go 项目结构:实践与模式

为可扩展性和清晰度构建你的 Go 项目

目录

构建 Go 项目结构 对于长期的可维护性、团队协作和可扩展性至关重要。与强制使用固定目录布局的框架不同,Go 倡导灵活性——但这种自由也意味着需要选择适合项目特定需求的模式。

项目树

理解 Go 对项目结构的哲学

Go 的极简主义设计哲学也延伸到了项目组织上。该语言不强制规定特定的结构,而是信任开发者做出明智的决策。这种做法促使社区发展出了一些经过验证的模式,从小型项目的简单扁平布局到企业系统的复杂架构。

关键原则是 优先简洁,必要时再引入复杂性。许多开发者在初始结构上容易陷入过度设计的陷阱,创建出深度嵌套的目录和过早的抽象。从今天所需的内容开始,随着项目的增长再进行重构。

我应该使用 internal/ 目录还是 pkg/ 目录?

internal/ 目录在 Go 的设计中有一个特定的用途:它包含不能被外部项目导入的包。Go 编译器强制执行这一限制,使 internal/ 成为私有应用逻辑、业务规则和不打算在项目外部重复使用的实用工具的完美选择。

另一方面,pkg/ 目录表明代码是供外部使用。只有在构建库或可重复使用的组件时,才应将代码放在这里。许多项目根本不需要 pkg/——如果代码不被外部使用,将其保留在根目录或 internal/ 中更整洁。在构建可重复使用的库时,考虑使用 Go 泛型 来创建类型安全、可重用的组件。

标准 Go 项目布局

最广泛认可的模式是 golang-standards/project-layout,但需要注意,这不是官方标准。一个典型的结构如下所示:

myproject/
├── cmd/
│   ├── api/
│   │   └── main.go
│   └── worker/
│       └── main.go
├── internal/
│   ├── auth/
│   ├── storage/
│   └── transport/
├── pkg/
│   ├── logger/
│   └── crypto/
├── api/
│   └── openapi.yaml
├── config/
│   └── config.yaml
├── scripts/
│   └── deploy.sh
├── go.mod
├── go.sum
└── README.md

cmd/ 目录

cmd/ 目录包含你的应用程序入口点。每个子目录代表一个独立的可执行二进制文件。例如,cmd/api/main.go 构建你的 API 服务器,而 cmd/worker/main.go 可能构建一个后台作业处理器。

最佳实践:保持你的 main.go 文件尽可能简洁——只需足够的代码来连接依赖项、加载配置并启动应用程序。所有实质性逻辑应属于 main.go 导入的包。

// cmd/api/main.go
package main

import (
    "log"
    "myproject/internal/server"
    "myproject/internal/config"
)

func main() {
    cfg, err := config.Load()
    if err != nil {
        log.Fatal(err)
    }
    
    srv := server.New(cfg)
    if err := srv.Start(); err != nil {
        log.Fatal(err)
    }
}

internal/ 目录

这是你的私有应用代码所在的地方。Go 编译器阻止任何外部项目导入 internal/ 中的包,这使其非常适合:

  • 业务逻辑和领域模型
  • 应用服务
  • 内部 API 和接口
  • 数据库仓库(如需选择合适的 ORM,请参阅我们的 PostgreSQL Go ORM 对比)
  • 认证和授权逻辑

按功能或领域组织 internal/,而不是按技术层。避免使用 internal/handlers/internal/services/internal/repositories/,而是使用 internal/user/internal/order/internal/payment/,其中每个包包含其处理程序、服务和仓库。

我是否应该从复杂的目录结构开始?

绝对不要。如果你正在构建一个小工具或原型,从以下结构开始:

myproject/
├── main.go
├── go.mod
└── go.sum

随着项目的发展和你识别出逻辑分组时,再引入目录。你可能在数据库逻辑变得重要时添加一个 db/ 包,或在 HTTP 处理程序增多时添加一个 api/ 包。让结构自然地浮现,而不是一开始就强加。

扁平结构与嵌套结构:找到平衡

Go 项目结构中最常见的错误之一是过度嵌套。Go 倾向于浅层的层次结构——通常是一到两层深。深层嵌套会增加认知负担并使导入变得繁琐。

最常见的错误有哪些?

过度嵌套目录:避免像 internal/services/user/handlers/http/v1/ 这样的结构。这会创建不必要的导航复杂性。相反,使用 internal/user/handler.go

通用的包名:如 utilshelperscommonbase 这样的名称是代码异味。它们没有传达特定功能,通常会成为不相关代码的倾倒地。使用描述性名称:validatorauthstoragecache

循环依赖:当包 A 导入包 B,而 B 又导入 A 时,就会产生循环依赖——这是 Go 中的编译错误。这通常表明关注点分离不佳。引入接口或提取共享类型到单独的包中。

混合关注点:保持 HTTP 处理程序专注于 HTTP 关注点,数据库仓库专注于数据访问,业务逻辑在服务包中。将业务规则放在处理程序中会使测试变得困难,并将你的领域逻辑与 HTTP 耦合。

领域驱动设计与六边形架构

对于较大的应用程序,特别是微服务,结合领域驱动设计(DDD)的六边形架构提供了清晰的关注点分离。

如何按照领域驱动设计结构化微服务?

六边形架构将代码组织成同心层,依赖关系向内流动:

internal/
├── domain/
│   └── user/
│       ├── entity.go        # 领域模型和值对象
│       ├── repository.go    # 仓库接口(端口)
│       └── service.go       # 领域服务
├── application/
│   └── user/
│       ├── create_user.go   # 用例:创建用户
│       ├── get_user.go      # 用例:检索用户
│       └── service.go       # 应用服务编排
├── adapter/
│   ├── http/
│   │   └── user_handler.go  # HTTP 适配器(REST 端点)
│   ├── postgres/
│   │   └── user_repo.go     # 数据库适配器(实现仓库端口)
│   └── redis/
│       └── cache.go         # 缓存适配器
└── api/
    └── http/
        └── router.go        # 路由配置和中间件

领域层domain/):核心业务逻辑、实体、值对象和领域服务接口。这一层对外部系统没有依赖——没有 HTTP、没有数据库导入。它定义了仓库接口(端口),适配器实现这些接口。

应用层application/):协调领域对象的用例。每个用例(如“创建用户”、“处理支付”)是一个单独的文件或包。这一层协调领域对象,但本身不包含业务规则。

适配器层adapter/):实现内部层定义的接口。HTTP 处理程序将请求转换为领域对象,数据库仓库实现持久化,消息队列处理异步通信。这一层包含所有框架特定和基础设施代码。

API 层api/):路由、中间件、DTO(数据传输对象)、API 版本控制和 OpenAPI 规范。

这种结构确保你的核心业务逻辑可测试且独立于框架、数据库或外部服务。你可以将 PostgreSQL 替换为 MongoDB,或将 REST 替换为 gRPC,而无需修改领域代码。

不同项目类型的实用模式

小型 CLI 工具

对于命令行应用程序,你希望结构支持多个命令和子命令。可以考虑使用 Cobra 作为命令结构,Viper 作为配置管理。我们的指南 使用 Cobra 与 Viper 在 Go 中构建 CLI 应用程序 详细介绍了这种模式。

mytool/
├── main.go
├── command/
│   ├── root.go
│   └── version.go
├── go.mod
└── README.md

REST API 服务

在 Go 中构建 REST API 时,这种结构清晰地分离了关注点:处理程序处理 HTTP 关注点,服务包含业务逻辑,仓库管理数据访问。对于涵盖标准库方法、框架、认证、测试模式和生产就绪最佳实践的全面指南,请参阅我们的 构建 Go 中 REST API 的完整指南

myapi/
├── cmd/
│   └── api/
│       └── main.go
├── internal/
│   ├── config/
│   ├── middleware/
│   ├── user/
│   │   ├── handler.go
│   │   ├── service.go
│   │   └── repository.go
│   └── product/
│       ├── handler.go
│       ├── service.go
│       └── repository.go
├── pkg/
│   └── httputil/
├── go.mod
└── README.md

包含多个服务的单体仓库

myproject/
├── cmd/
│   ├── api/
│   ├── worker/
│   └── scheduler/
├── internal/
│   ├── shared/        # 共享内部包
│   ├── api/          # API 特定代码
│   ├── worker/       # 工作器特定代码
│   └── scheduler/    # 调度器特定代码
├── pkg/              # 共享库
├── go.work           # Go 工作区文件
└── README.md

测试与文档

使用 _test.go 后缀将测试文件与被测试的代码放在一起:

internal/
└── user/
    ├── service.go
    ├── service_test.go
    ├── repository.go
    └── repository_test.go

这种惯例将测试与实现保持在一起,使它们易于查找和维护。对于涉及多个包的集成测试,可以在项目根目录创建一个单独的 test/ 目录。对于编写有效单元测试的全面指导,包括表格驱动测试、模拟、覆盖率分析和最佳实践,请参阅我们的 Go 单元测试结构与最佳实践指南

文档应放在以下位置:

  • README.md:项目概述、设置说明、基本用法
  • docs/:详细文档、架构决策、API 参考
  • api/:OpenAPI/Swagger 规范、protobuf 定义

对于 REST API,使用 Swagger 生成并提供 OpenAPI 文档对于 API 可发现性和开发人员体验至关重要。我们的指南 在 Go API 中添加 Swagger 覆盖了与流行框架的集成和最佳实践。

使用 Go 模块管理依赖

每个 Go 项目都应使用 Go 模块进行依赖管理。对于 Go 命令和模块管理的全面参考,请参阅我们的 Go 快速参考。初始化方法如下:

go mod init github.com/yourusername/myproject

这会创建 go.mod(依赖项和版本)和 go.sum(用于验证的校验和)。将这些文件纳入版本控制以实现可重复的构建。

定期更新依赖项:

go get -u ./...          # 更新所有依赖项
go mod tidy              # 删除未使用的依赖项
go mod verify            # 验证校验和

关键要点

  1. 从简单开始,自然演进:不要过度设计初始结构。在复杂性要求时再添加目录和包。

  2. 优先扁平层次结构:限制嵌套为一到两层。Go 的扁平包结构提高了可读性。

  3. 使用描述性包名:避免使用像 utils 这样的通用名称。根据包的功能命名:authstoragevalidator

  4. 清晰地分离关注点:保持处理程序专注于 HTTP,仓库专注于数据访问,业务逻辑在服务包中。

  5. 使用 internal/ 以实现隐私:将不应被外部导入的代码放在这里。大多数应用代码应属于此目录。

  6. 在需要时应用架构模式:对于复杂系统,六边形架构和 DDD 提供了清晰的边界和可测试性。

  7. 让 Go 引导你:遵循 Go 的惯用法,而不是从其他语言导入模式。Go 有自己关于简洁和组织的哲学。

有用的链接

其他相关文章