大家好,我是极客老墨。
好的项目结构能让代码更易理解、易维护、易扩展。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. 最佳实践
- 从简单开始:不要一开始就创建复杂的目录结构
- 按需扩展:当代码变多时再拆分目录
- 优先使用 internal:除非确定要公开,否则放在
internal/ - 保持一致:团队内统一目录结构
- 文档先行:在 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 个关键点:
- cmd/:程序入口,每个子目录对应一个可执行文件,main.go 要简洁
- internal/:私有代码,最常用,防止外部依赖内部实现
- pkg/:公共库,谨慎使用,只放稳定可复用的代码
- 从简单开始:不要一开始就创建复杂结构,按需扩展
- 保持一致:团队内统一目录结构,在 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 管理所有服务
思考题
- 为什么 Go 要设计 internal/ 这个特殊目录? 它解决了什么问题?
- 什么时候应该使用 pkg/,什么时候用 internal/? 有明确的判断标准吗?
- 单仓库(Monorepo)和多仓库(Multi-repo)各有什么优缺点? 如何选择?
- 项目结构和代码质量有什么关系? 好的结构能带来哪些好处?
大家可以在公众号后台讨论这些问题,老墨会挑选有价值的观点在后续文章中分享。
为什么 Go 语言要设计 internal/ 这个特殊目录?它解决了什么问题?欢迎评论区聊聊。
极客老墨,继续折腾
如果有任何问题,欢迎在评论区留言或关注公众号「极客老墨」交流。
完整示例代码在 go-tutorial-code/13-project-layout。