main.go 应放在 cmd/app/main.go,只负责初始化服务、加载配置和启动 HTTP server;handlers 比 controllers 更轻量清晰,推荐按业务域组织;service 层用于跨资源操作,依赖接口便于测试;internal/ 用于私有包,防止外部 import。

main.go 放哪?别塞进根目录
很多新手把 main.go 直接丢项目根目录,结果随着路由增多、中间件变复杂,main.go 迅速膨胀成 300 行+的“上帝文件”。它应该只做一件事:初始化服务、加载配置、启动 HTTP server。建议放在 cmd/app/main.go,和 cmd/admin/main.go(如果还有管理后台)分开,避免职责混杂。
常见错误是把数据库初始化、路由注册全写进 main.go —— 这会让测试入口难写、多环境启动逻辑耦合。正确做法是抽成 app.New() 或 server.NewServer() 函数,在 main.go 里只调用一次。
handlers 和 controllers 到底用哪个?Go 里推荐 handlers
Go 没有强约定 MVC,用 controllers 容易让人误以为要套 Ruby on Rails 那套生命周期。实际项目中更轻量、更清晰的做法是用 handlers:每个文件对应一个业务域(如 user/handler.go),函数签名统一为 func(w http.ResponseWriter, r *http.Request) 或封装后的 http.HandlerFunc。
- 路由注册时直接传入,不加额外 wrapper
- 便于单元测试:构造
httptest.NewRequest和httptest.ResponseRecorder即可覆盖逻辑 - 避免过度抽象——比如
UserController.Create()最终还是得读 body、校验、调 service、写响应,不如直写user.CreateHandler
service 层不是必须的,但 domain/service 分离能防烂代码
小项目可以跳过 service 目录,让 handler 直接调用 repo.UserRepo。但一旦出现跨资源操作(如创建用户同时发邮件、扣积分、写日志),逻辑就会散落在多个 handler 里。这时建 service/user_service.go 就很有必要。
立即学习“go语言免费学习笔记(深入)”;
关键点在于:service 层不处理 HTTP 细节(无 http.ResponseWriter),只接收 domain model(如 user.User),返回 error 或 domain struct。它依赖的是接口,比如:
type UserRepository interface {
Create(u *user.User) error
FindByID(id int) (*user.User, error)
}
这样测试时可用 mock 实现替换数据库,也方便未来切换存储层。
pkg vs internal:什么时候该用 internal?
pkg/ 适合放可被其他项目 import 的通用工具,比如 pkg/log、pkg/validator;而 internal/ 是 Go 官方支持的“私有包”机制——放在 internal/ 下的包,**外部模块无法 import**,哪怕路径对也不行。
真实项目里,internal/handler、internal/service、internal/repo 应该是主力目录。它们只供本项目二进制使用,不会暴露 API 给别人。否则容易出现 “我改了个 repo 方法,结果 CI 爆了三个下游项目” 这种事。
容易踩的坑:internal 必须是目录名,不能是 internalpkg 或 internals;且它的父级不能是 vendor 或另一个 internal。
internal 隔离、越晚往 main.go 塞逻辑,后期改起来就越少想删库跑路。










