引入
Go 语法学会了,模块也会用了,真要写一个项目时,文件往哪放?目录怎么分?Go 社区有一套约定俗成的项目结构,虽然不是强制标准,但遵循它能让项目清晰易懂、方便协作。
正文
定义
定义
Go 项目结构是社区推荐的标准目录布局,把代码按职责分到不同目录。
cmd/放可执行程序的入口,internal/放仅项目内部使用的代码,pkg/放可以被外部引用的公共库。遵循约定能让项目结构一目了然。
语法
project/
├── cmd/
│ └── app/
│ └── main.go # 程序入口
├── internal/
│ ├── handler/ # 仅内部使用的包
│ └── service/
├── pkg/
│ └── utils/ # 可被外部引用的包
├── api/
│ └── v1/ # API 定义
├── configs/ # 配置文件
├── scripts/ # 脚本
├── go.mod
├── go.sum
├── Makefile
└── README.md
例子
cmd/ — 程序入口
cmd/
├── server/
│ └── main.go # HTTP 服务器入口
└── cli/
└── main.go # 命令行工具入口
// cmd/server/main.go
package main
import (
"fmt"
"myproject/internal/server"
)
func main() {
fmt.Println("启动服务器...")
server.Run(":8080")
}cmd/ 下放每个可执行程序的 main.go,只做最少的启动逻辑,真正的业务逻辑放到 internal/ 或 pkg/ 里。一个项目可以有多个程序入口,用子目录区分。
internal/ — 项目内部代码
internal/
├── auth/
│ └── auth.go
├── handler/
│ └── user.go
├── service/
│ └── user.go
└── repository/
└── user.go
// internal/service/user.go
package service
import "myproject/internal/repository"
type UserService struct {
repo *repository.UserRepo
}
func (s *UserService) GetUser(id int) (string, error) {
return s.repo.FindByID(id)
}internal/ 下的包只能被本项目引用,外部项目 import 会编译报错。Go 编译器强制保证这一点。适合放不想被外部依赖的业务代码。
pkg/ — 公共库代码
pkg/
├── httputil/
│ └── response.go
└── validator/
└── validator.go
// pkg/httputil/response.go
package httputil
import (
"encoding/json"
"net/http"
)
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
func JSON(w http.ResponseWriter, code int, resp Response) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(resp)
}pkg/ 下的包可以被外部项目引用,适合放通用的工具代码。如果某个包不确定要不要给别人用,先放 internal/,以后需要时再移到 pkg/。
完整项目示例
myapp/
├── cmd/
│ └── myapp/
│ └── main.go
├── internal/
│ ├── config/
│ │ └── config.go
│ ├── handler/
│ │ ├── user.go
│ │ └── order.go
│ ├── middleware/
│ │ └── auth.go
│ ├── service/
│ │ ├── user.go
│ │ └── order.go
│ └── repository/
│ ├── user.go
│ └── order.go
├── pkg/
│ └── errcode/
│ └── errcode.go
├── api/
│ └── v1/
│ └── types.go
├── configs/
│ └── config.yaml
├── migrations/
│ └── 001_init.sql
├── go.mod
├── go.sum
├── Makefile
├── Dockerfile
└── README.md
// cmd/myapp/main.go
package main
import (
"myproject/internal/config"
"myproject/internal/handler"
)
func main() {
cfg := config.Load()
handler.StartServer(cfg.Port)
}入口文件只做组装和启动,把配置读取、路由注册等细节委托给 internal/ 里的包。
小项目不需要复杂结构
myapp/
├── main.go
├── handler.go
├── model.go
├── go.mod
└── go.sum
小项目不需要硬套标准布局,一个目录放所有文件也完全没问题。等项目长大后再拆分目录。过早引入复杂结构反而是负担。
常见写法
| 目录 | 说明 |
|---|---|
cmd/ | 可执行程序入口(main.go) |
internal/ | 仅项目内部使用的包(编译器强制) |
pkg/ | 可被外部引用的公共库 |
api/ | API 定义(proto、OpenAPI 等) |
configs/ | 配置文件 |
scripts/ | 构建/部署脚本 |
migrations/ | 数据库迁移文件 |
test/ | 测试工具、测试数据 |
vendor/ | 依赖源码(go mod vendor 生成) |
特点
cmd/放main.go,只做启动逻辑,业务代码放别处internal/被 Go 编译器强制保护,外部项目不能引用pkg/是可选的,放通用工具代码,可以被外部引用- 标准布局是社区约定,不是强制标准
- 小项目不需要复杂结构,先简单后拆分
- 一个项目可以有多个
cmd/子目录,对应多个可执行文件 go.mod和go.sum放在项目根目录README.md、Makefile、Dockerfile也放在根目录
理解
Go 项目结构的核心思路就三个抽屉:cmd/ 放启动入口,internal/ 放自己用的代码,pkg/ 放能给别人用的代码。不用一上来就搞这么复杂,小项目一个目录就够了,等代码量上来了再按职责拆目录。记住 internal/ 是被编译器保护的,放进去的东西外面拿不到,这是 Go 独有的安全机制。
引出
项目结构搞定了,Go 从语法基础到并发编程再到项目管理就都有个全貌了。写代码还有一件事不能忘——保持代码格式统一。接下来看 Go fmt 包,学习 Go 内置的代码格式化工具。