引入

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.modgo.sum 放在项目根目录
  • README.mdMakefileDockerfile 也放在根目录

理解

Go 项目结构的核心思路就三个抽屉:cmd/ 放启动入口,internal/ 放自己用的代码,pkg/ 放能给别人用的代码。不用一上来就搞这么复杂,小项目一个目录就够了,等代码量上来了再按职责拆目录。记住 internal/ 是被编译器保护的,放进去的东西外面拿不到,这是 Go 独有的安全机制。

引出

项目结构搞定了,Go 从语法基础到并发编程再到项目管理就都有个全貌了。写代码还有一件事不能忘——保持代码格式统一。接下来看 Go fmt 包,学习 Go 内置的代码格式化工具。