大家好,我是极客老墨。

好的项目结构能让代码更易理解、易维护、易扩展。Go 社区有一套被广泛认可的标准布局,这篇就聊聊如何组织 Go 项目,让代码结构清晰合理。

1. 标准项目布局

参考:golang-standards/project-layout

 1myproject/
 2├── cmd/                    # 主程序入口
 3│   ├── server/
 4│   │   └── main.go        # 服务端入口
 5│   └── cli/
 6│       └── main.go        # 命令行工具入口
 7├── internal/              # 私有代码(不可被外部导入)
 8│   ├── handler/
 9│   ├── service/
10│   └── repository/
11├── pkg/                   # 公共库(可被外部导入)
12│   ├── util/
13│   └── validator/
14├── api/                   # API 定义(OpenAPI/Swagger)
15│   └── openapi.yaml
16├── web/                   # Web 静态资源
17│   ├── static/
18│   └── templates/
19├── configs/               # 配置文件
20│   ├── config.yaml
21│   └── config.prod.yaml
22├── scripts/               # 脚本(构建、部署等)
23│   ├── build.sh
24│   └── deploy.sh
25├── test/                  # 额外的测试数据
26│   └── fixtures/
27├── docs/                  # 文档
28│   └── API.md
29├── go.mod
30├── go.sum
31├── Makefile              # 构建脚本
32├── Dockerfile
33└── README.md

2. 核心目录详解

2.1 cmd/

存放主程序入口,每个子目录对应一个可执行文件。

1cmd/
2├── server/
3│   └── main.go        # go build -o server ./cmd/server
4├── worker/
5│   └── main.go        # go build -o worker ./cmd/worker
6└── migrate/
7    └── main.go        # go build -o migrate ./cmd/migrate

main.go 应该尽量简洁

 1// cmd/server/main.go
 2package main
 3
 4import (
 5    "myproject/internal/server"
 6)
 7
 8func main() {
 9    server.Run()  // 具体逻辑在 internal/server 中
10}

2.2 internal/

私有代码,只能被本项目导入,外部项目无法导入。

1internal/
2├── handler/      # HTTP 处理器
3├── service/      # 业务逻辑
4├── repository/   # 数据访问
5├── model/        # 数据模型
6└── middleware/   # 中间件

为什么需要 internal?

  • 防止外部项目依赖你的内部实现
  • 给你重构的自由(不用担心破坏外部依赖)

2.3 pkg/

公共库,可以被外部项目导入。

1pkg/
2├── util/         # 通用工具函数
3├── validator/    # 验证器
4└── logger/       # 日志封装

何时使用 pkg?

  • 代码足够通用,可以被其他项目复用
  • 已经稳定,不会频繁变动

注意:很多项目不需要 pkg/,所有代码都放在 internal/ 即可。

2.4 api/

存放 API 定义文件:

  • OpenAPI/Swagger 规范
  • Protocol Buffers (.proto 文件)
  • GraphQL schema
1api/
2├── openapi.yaml
3├── user.proto
4└── schema.graphql

2.5 configs/

配置文件模板(不包含敏感信息)。

1configs/
2├── config.yaml          # 开发环境配置
3├── config.prod.yaml     # 生产环境配置模板
4└── config.example.yaml  # 配置示例

敏感信息应该通过环境变量传入

3. 其他常见目录

3.1 build/

构建相关文件:

1build/
2├── package/      # 打包脚本
3└── ci/           # CI 配置

3.2 deployments/

部署配置:

1deployments/
2├── docker-compose.yml
3├── kubernetes/
4│   ├── deployment.yaml
5│   └── service.yaml
6└── terraform/

3.3 vendor/

依赖的副本(可选):

1go mod vendor

通常不需要提交到版本控制。

4. 小型项目结构

对于简单项目,可以简化:

1simple-project/
2├── main.go           # 单文件入口
3├── handler.go        # HTTP 处理器
4├── model.go          # 数据模型
5├── go.mod
6└── README.md

或者:

 1simple-project/
 2├── cmd/
 3│   └── server/
 4│       └── main.go
 5├── internal/
 6│   ├── handler/
 7│   ├── service/
 8│   └── model/
 9├── go.mod
10└── README.md

5. 中型项目结构

 1medium-project/
 2├── cmd/
 3│   └── server/
 4│       └── main.go
 5├── internal/
 6│   ├── handler/
 7│   │   ├── user.go
 8│   │   └── post.go
 9│   ├── service/
10│   │   ├── user.go
11│   │   └── post.go
12│   ├── repository/
13│   │   ├── user.go
14│   │   └── post.go
15│   ├── model/
16│   │   └── model.go
17│   └── middleware/
18│       └── auth.go
19├── pkg/
20│   └── response/
21│       └── response.go
22├── configs/
23│   └── config.yaml
24├── go.mod
25├── Dockerfile
26└── README.md

6. 大型项目结构(微服务)

 1large-project/
 2├── services/
 3│   ├── user-service/
 4│   │   ├── cmd/
 5│   │   ├── internal/
 6│   │   └── go.mod
 7│   ├── order-service/
 8│   │   ├── cmd/
 9│   │   ├── internal/
10│   │   └── go.mod
11│   └── payment-service/
12│       ├── cmd/
13│       ├── internal/
14│       └── go.mod
15├── pkg/                # 跨服务共享的代码
16│   ├── logger/
17│   └── config/
18└── api/                # API 定义
19    └── proto/

7. 最佳实践

  1. 从简单开始:不要一开始就创建复杂的目录结构
  2. 按需扩展:当代码变多时再拆分目录
  3. 优先使用 internal:除非确定要公开,否则放在 internal/
  4. 保持一致:团队内统一目录结构
  5. 文档先行:在 README 中说明项目结构

8. 常见错误

8.1 过度设计

 1❌ 不好:一开始就创建大量空目录
 2myproject/
 3├── cmd/
 4├── internal/
 5├── pkg/
 6├── api/
 7├── web/
 8├── configs/
 9├── scripts/
10├── test/
11└── docs/
1✅ 好:从简单开始
2myproject/
3├── main.go
4├── handler.go
5└── go.mod

8.2 混淆 internal 和 pkg

  • internal/:私有代码,频繁变动
  • pkg/:公共库,稳定可复用

大多数项目只需要 internal/

8.3 main.go 太复杂

 1// ❌ 不好:main.go 包含大量业务逻辑
 2func main() {
 3    db := connectDB()
 4    router := setupRouter()
 5    // ... 100 行代码
 6}
 7
 8// ✅ 好:main.go 只负责启动
 9func main() {
10    server.Run()
11}

老墨踩过的坑

坑 1:一开始就创建复杂的目录结构

老墨刚学 Go 时,看到标准布局就全部照搬:

 1❌ 错误:项目只有 3 个文件,却创建了 10 个目录
 2myproject/
 3├── cmd/
 4├── internal/
 5├── pkg/
 6├── api/
 7├── web/
 8├── configs/
 9├── scripts/
10├── test/
11├── docs/
12└── build/

问题:大量空目录,增加复杂度,反而不利于开发。

正确做法

 1✅ 正确:从简单开始
 2myproject/
 3├── main.go
 4├── handler.go
 5├── model.go
 6└── go.mod
 7
 8# 当代码增多时再拆分
 9myproject/
10├── cmd/
11│   └── server/
12│       └── main.go
13├── internal/
14│   ├── handler/
15│   └── model/
16└── go.mod

坑 2:把所有代码都放在 pkg/

1❌ 错误:把内部实现放在 pkg/
2myproject/
3├── cmd/
4└── pkg/
5    ├── handler/      # 这些应该在 internal/
6    ├── service/
7    └── repository/

问题:pkg/ 的代码可以被外部导入,失去了重构的自由。

正确做法

1✅ 正确:内部实现放在 internal/
2myproject/
3├── cmd/
4├── internal/         # 私有代码
5│   ├── handler/
6│   ├── service/
7│   └── repository/
8└── pkg/              # 只放真正通用的工具
9    └── logger/

坑 3:main.go 包含大量业务逻辑

 1// ❌ 错误:main.go 太复杂
 2func main() {
 3    // 连接数据库
 4    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
 5    if err != nil {
 6        panic(err)
 7    }
 8    
 9    // 初始化 Redis
10    rdb := redis.NewClient(&redis.Options{
11        Addr: "localhost:6379",
12    })
13    
14    // 设置路由
15    r := gin.Default()
16    r.GET("/users", func(c *gin.Context) {
17        // ... 100 行业务逻辑
18    })
19    
20    // ... 更多代码
21    r.Run(":8080")
22}

问题:main.go 难以测试,逻辑混乱。

正确做法

 1// ✅ 正确:main.go 只负责启动
 2// cmd/server/main.go
 3package main
 4
 5import "myproject/internal/server"
 6
 7func main() {
 8    server.Run()
 9}
10
11// internal/server/server.go
12package server
13
14func Run() {
15    db := initDB()
16    rdb := initRedis()
17    router := setupRouter(db, rdb)
18    router.Run(":8080")
19}

坑 4:配置文件包含敏感信息

1# ❌ 错误:configs/config.yaml 包含密码
2database:
3  host: localhost
4  port: 3306
5  username: root
6  password: "MySecretPassword123"  # 不要提交到 Git!

问题:敏感信息泄露到版本控制系统(如果是内部私有仓库随意,但老墨建议使用环境变量)。

正确做法

 1# ✅ 正确:configs/config.yaml 只有模板
 2database:
 3  host: ${DB_HOST}
 4  port: ${DB_PORT}
 5  username: ${DB_USER}
 6  password: ${DB_PASSWORD}  # 从环境变量读取
 7
 8# .env.example(提交到 Git)
 9DB_HOST=localhost
10DB_PORT=3306
11DB_USER=root
12DB_PASSWORD=your_password_here
13
14# .env(不提交到 Git)
15DB_HOST=localhost
16DB_PORT=3306
17DB_USER=root
18DB_PASSWORD=MySecretPassword123

坑 5:internal 和 pkg 边界不清

1❌ 错误:不知道该放哪儿
2myproject/
3├── internal/
4│   └── util/         # 通用工具,应该在 pkg/
5└── pkg/
6    └── handler/      # HTTP 处理器,应该在 internal/

判断标准

  • internal/:只被本项目使用,可能频繁变动
  • pkg/:可以被其他项目复用,API 稳定

正确做法

1✅ 正确:明确边界
2myproject/
3├── internal/         # 项目特定的代码
4│   ├── handler/      # HTTP 处理器
5│   ├── service/      # 业务逻辑
6│   └── repository/   # 数据访问
7└── pkg/              # 通用工具(可选)
8    ├── logger/       # 日志封装
9    └── validator/    # 验证器

实战建议

1. 项目规模决定结构复杂度

 1# 小项目(<1000 行)
 2myproject/
 3├── main.go
 4├── handler.go
 5├── model.go
 6└── go.mod
 7
 8# 中型项目(1000-10000 行)
 9myproject/
10├── cmd/
11│   └── server/
12│       └── main.go
13├── internal/
14│   ├── handler/
15│   ├── service/
16│   └── model/
17├── configs/
18│   └── config.yaml
19└── go.mod
20
21# 大型项目(>10000 行)
22myproject/
23├── cmd/
24│   ├── server/
25│   ├── worker/
26│   └── cli/
27├── internal/
28│   ├── handler/
29│   ├── service/
30│   ├── repository/
31│   ├── model/
32│   └── middleware/
33├── pkg/
34│   ├── logger/
35│   └── util/
36├── api/
37├── configs/
38├── scripts/
39└── go.mod

2. 使用 Makefile 管理项目

 1# Makefile
 2.PHONY: build run test clean
 3
 4# 构建
 5build:
 6	go build -o bin/server ./cmd/server
 7	go build -o bin/worker ./cmd/worker
 8
 9# 运行
10run:
11	go run ./cmd/server
12
13# 测试
14test:
15	go test -v ./...
16
17# 清理
18clean:
19	rm -rf bin/
20
21# 代码检查
22lint:
23	golangci-lint run
24
25# 生成文档
26docs:
27	swag init -g cmd/server/main.go

3. 分层架构示例

保持与传统 MVC 架构一致。

 1internal/
 2├── handler/          # 表现层(HTTP/gRPC)
 3│   ├── user.go
 4│   └── post.go
 5├── service/          # 业务逻辑层
 6│   ├── user.go
 7│   └── post.go
 8├── repository/       # 数据访问层
 9│   ├── user.go
10│   └── post.go
11└── model/            # 数据模型
12    └── model.go
13
14# 依赖方向:handler → service → repository

4. 配置管理最佳实践

 1// internal/config/config.go
 2package config
 3
 4import (
 5    "github.com/spf13/viper"
 6)
 7
 8type Config struct {
 9    Server   ServerConfig
10    Database DatabaseConfig
11    Redis    RedisConfig
12}
13
14type ServerConfig struct {
15    Port int
16    Mode string
17}
18
19type DatabaseConfig struct {
20    Host     string
21    Port     int
22    Username string
23    Password string
24    Database string
25}
26
27func Load(path string) (*Config, error) {
28    viper.SetConfigFile(path)
29    viper.AutomaticEnv() // 自动读取环境变量
30    
31    if err := viper.ReadInConfig(); err != nil {
32        return nil, err
33    }
34    
35    var cfg Config
36    if err := viper.Unmarshal(&cfg); err != nil {
37        return nil, err
38    }
39    
40    return &cfg, nil
41}

5. 多入口项目示例

 1cmd/
 2├── server/           # Web 服务器
 3│   └── main.go
 4├── worker/           # 后台任务
 5│   └── main.go
 6├── migrate/          # 数据库迁移
 7│   └── main.go
 8└── cli/              # 命令行工具
 9    └── main.go
10
11# 构建
12go build -o bin/server ./cmd/server
13go build -o bin/worker ./cmd/worker
14go build -o bin/migrate ./cmd/migrate
15go build -o bin/cli ./cmd/cli

6. 微服务项目结构

 1# 方式 1:Monorepo(单仓库多服务)
 2myproject/
 3├── services/
 4│   ├── user-service/
 5│   │   ├── cmd/
 6│   │   ├── internal/
 7│   │   └── go.mod
 8│   ├── order-service/
 9│   │   ├── cmd/
10│   │   ├── internal/
11│   │   └── go.mod
12│   └── payment-service/
13│       ├── cmd/
14│       ├── internal/
15│       └── go.mod
16├── pkg/              # 共享代码
17│   ├── logger/
18│   └── config/
19└── api/              # API 定义
20    └── proto/
21
22# 方式 2:Multi-repo(多仓库)
23user-service/         # 独立仓库
24├── cmd/
25├── internal/
26└── go.mod
27
28order-service/        # 独立仓库
29├── cmd/
30├── internal/
31└── go.mod

老墨总结

Go 项目结构的 5 个关键点:

  1. cmd/:程序入口,每个子目录对应一个可执行文件,main.go 要简洁
  2. internal/:私有代码,最常用,防止外部依赖内部实现
  3. pkg/:公共库,谨慎使用,只放稳定可复用的代码
  4. 从简单开始:不要一开始就创建复杂结构,按需扩展
  5. 保持一致:团队内统一目录结构,在 README 中说明

实战建议

  • 根据项目规模选择结构:小项目简单,大项目完整
  • 优先使用 internal/,除非确定要公开才用 pkg/
  • main.go 只负责启动,具体逻辑放在 internal/
  • 配置文件不包含敏感信息,使用环境变量
  • 使用 Makefile 管理构建、测试、部署
  • 分层架构:handler → service → repository

为什么 Go 语言要设计 internal/ 这个特殊目录?它解决了什么问题?欢迎评论区聊聊。

极客老墨,继续折腾!

练习题

练习题 1:创建标准项目结构(⭐)

创建一个标准的 Go 项目结构(cmd + internal + configs)。

要求:

  • 使用 go mod init 初始化
  • 创建 cmd/server 目录
  • 创建 internal/handler、internal/service 目录
  • 创建 configs 目录和配置文件

练习题 2:多入口项目(⭐⭐)

实现一个多入口项目(server + worker + cli)。

要求:

  • 三个入口共享 internal/ 代码
  • 每个入口有独立的 main.go
  • 使用 Makefile 管理构建
  • 测试三个程序都能正常运行

练习题 3:理解 internal 的作用(⭐⭐)

尝试从外部导入 internal 包,观察编译错误。

要求:

  • 创建两个模块:projectA 和 projectB
  • projectA 有 internal/util 包
  • projectB 尝试导入 projectA 的 internal/util
  • 理解为什么会编译失败

练习题 4:pkg vs internal(⭐⭐⭐)

对比 pkg 和 internal 的使用场景。

要求:

  • 创建一个项目,同时有 pkg/ 和 internal/
  • 在 pkg/ 中放通用工具(如 logger)
  • 在 internal/ 中放业务逻辑
  • 从另一个项目导入 pkg/ 的代码

练习题 5:重构现有项目(⭐⭐⭐)

重构一个现有项目,使用标准布局。

要求:

  • 选择一个单文件项目
  • 按照标准布局重构
  • 拆分 handler、service、repository 层
  • 确保功能不变

练习题 6:微服务项目结构(⭐⭐⭐)

设计一个微服务项目的目录结构。

要求:

  • 至少 3 个服务(user、order、payment)
  • 使用 Monorepo 方式组织
  • 创建共享的 pkg/ 目录
  • 编写 Makefile 管理所有服务

思考题

  1. 为什么 Go 要设计 internal/ 这个特殊目录? 它解决了什么问题?
  2. 什么时候应该使用 pkg/,什么时候用 internal/? 有明确的判断标准吗?
  3. 单仓库(Monorepo)和多仓库(Multi-repo)各有什么优缺点? 如何选择?
  4. 项目结构和代码质量有什么关系? 好的结构能带来哪些好处?

大家可以在公众号后台讨论这些问题,老墨会挑选有价值的观点在后续文章中分享。


为什么 Go 语言要设计 internal/ 这个特殊目录?它解决了什么问题?欢迎评论区聊聊。

极客老墨,继续折腾

如果有任何问题,欢迎在评论区留言或关注公众号「极客老墨」交流。

完整示例代码在 go-tutorial-code/13-project-layout


相关阅读