cmd 目录仅存放 main 函数,负责解析配置、构建依赖、调用 app.Run();业务逻辑应置于 internal 或 pkg,其中 internal 为私有模块,pkg 为可复用的公共工具。

cmd 目录只放 main 函数,别塞业务逻辑
Go 项目里 cmd 是入口的“门面”,不是业务代码仓库。它的唯一职责就是初始化配置、启动服务、调用 internal 或 pkg 提供的接口。一旦你在 cmd/main.go 里写数据库查询、HTTP 路由注册、中间件组装——说明目录结构已经失控了。
常见错误现象:cmd/myapp/main.go 超过 200 行,里面有 sql.Open、http.HandleFunc、甚至硬编码的 JWT 密钥。
- 每个可执行命令对应一个子目录(如
cmd/api、cmd/worker),避免混在一起 -
main()函数里只做三件事:解析 flag / env、构造顶层依赖(如 logger、DB)、调用app.Run()这类封装好的启动函数 - 不 import
internal以外的业务包(比如别直接 importmodels或handlers)
internal 和 pkg 的边界:能被外部复用才进 pkg
internal 是你的项目私有领地,Go 编译器会阻止其他模块 import 它;pkg 是你愿意对外暴露的“工具箱”。别因为“看着顺眼”就把通用函数扔进 pkg——得真有人在别的项目里想用它。
使用场景举例:你写了带重试的 HTTP client 封装,如果未来可能被公司其他 Go 服务复用,就放 pkg/httpclient;但如果只给本项目里的 worker 调用,就该在 internal/worker/httpclient 里。
-
pkg下的代码必须有完整测试、明确文档注释(// Package httpclient provides...) -
internal可以用短命名(如store、svc),pkg命名要更稳定(如httpx、uuidx) - 如果某个
pkg模块只被本项目 import,且半年没被外部引用过,考虑迁回internal
不要为了“规范”而建空目录或过度分层
刚起步的项目,internal/handler、internal/service、internal/repository 全都建好,但每个目录下只有 1 个文件、总共不到 50 行代码——这是在给自己加心智负担,不是工程化。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
参数差异体现在演化节奏上:小项目从 internal/app 开始就够了;等 handler 超过 3 个、service 逻辑开始交叉、SQL 查询分散在多个地方时,再按关注点拆。
- 先有
internal/app,里面放App结构体和Run()方法 - 当路由逻辑膨胀,再拆出
internal/handler;当 DB 操作重复出现,再拉出internal/repo - 避免出现
internal/core/domain/entity这种四层嵌套——Go 不需要 DDD 的仪式感
go mod tidy 会暴露目录设计缺陷
运行 go mod tidy 后,如果提示 found packages xxx and yyy in ...,大概率是目录结构破坏了 Go 的包隔离规则:比如在 internal/api 和 internal/api/v2 里用了同名包名 api;或者把两个不同用途的模块放在同一目录却没拆包。
性能影响不大,但会让依赖图混乱,CI 里 go list -deps 输出难以解读,重构时不敢删目录。
- 每个目录必须对应唯一包名,且包名应反映其角色(如
repo、cache、grpcsrv),不用跟目录名完全一致 -
internal下禁止出现main包(除了cmd) - 如果某目录下同时存在
.go和.proto文件,确保protoc生成的代码不在同一包内,否则go build会报错
最常被忽略的是:目录名本身不是 API,真正影响协作的是包名 + import 路径 + 接口定义。别花一整天纠结 “该叫 dal 还是 data”,先让 internal/repo 能被 internal/handler 干净地依赖住再说。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










