main.go 应放在 cmd/yourapp/ 下,如 cmd/api/main.go;根目录只留 go.mod 等配置文件;internal/ 用于私有包,config/ 存项目专属配置,pkg/ 存可独立 go get 的通用组件。

main.go 到底该放哪?别让它在根目录“裸奔”
Go 项目里,main.go 必须放在 package main 下,但它绝不该孤零零躺在项目根目录——这是绝大多数新手踩进的第一个坑。根目录塞了 main.go,很快就会混进 config.yaml、router.go、db.go,最后 go list ./ 都可能报错 “multiple main packages”。
-
cmd/yourapp/main.go是标准解法:一个服务一个子目录,比如cmd/api/main.go启 HTTP 服务,cmd/migrate/main.go跑迁移 -
main.go里只做三件事:加载配置、初始化依赖(DB、logger、router)、调用server.ListenAndServe()—— 别写 SQL、别注册路由、别解析环境变量 - 根目录只留
go.mod、Makefile、.gitignore;运行时用go run cmd/api/main.go,构建用go build -o bin/api cmd/api
internal/ 不是“代码回收站”,它有明确的导入边界
internal/ 是 Go 官方约定的私有包路径,不是让你把所有“暂时不想公开”的代码都扔进去的地方。它的作用很硬:任何在 github.com/user/project/internal/xxx 下的包,外部模块(包括你自己的 cmd/ 以外的其他仓库)根本 import 不了。
- 正确用法:
internal/handler(HTTP 入口)、internal/service(业务逻辑)、internal/repository(数据访问接口)、internal/model(领域结构体) - 常见误用:
internal/utils放通用字符串函数 → 应挪到pkg/utils或直接拆成独立模块;internal/db里硬编码gorm.Open()→ DB 初始化应由cmd/控制,internal/repository只依赖抽象接口 - 模型(如
Userstruct)统一放internal/model,别在handler和repository里各自定义一份
config/ 和 pkg/ 怎么分?看它会不会被另一个项目 go get
配置加载逻辑和通用工具包,名字像、位置近,但职责天差地别。混淆这两者,轻则导致项目耦合变重,重则让别人误引你内部实现细节。
在 Go 中使用 google/wire 实现编译时依赖注入——wire.NewSet、wire.Build、wire.Bind(接口→实现)、wire.Struct、wire.Value、wire.Interface
-
config/是项目专属:读config.development.yaml、绑定环境变量前缀(如APP_)、校验DB.URL是否非空 —— 所有代码只在本项目跑,结构体定义在internal/config/config.go,用viper.Unmarshal()绑定 -
pkg/是“可交付组件”:比如pkg/logger封装zerolog并暴露Logger接口;pkg/httpclient提供带默认超时和 metrics 的 client 工厂 —— 它们必须能被go get github.com/user/pkg/logger独立使用 - 没想清楚要不要复用?先别建
pkg/。90% 的“通用工具”最后只在本项目用三次,硬塞进去反而增加维护负担
handlers/ 还是 api/?命名要让人一眼看懂它干啥
HTTP 处理层的目录名,本质是团队沟通契约。别用模糊词如 routes/、web/、controllers/ —— Go 社区没有 controller 概念,routes 又容易和路由注册逻辑混淆。
- 推荐用
api/或handlers/:前者强调“对外暴露的接口集合”,后者更贴近 Go 原生http.Handler语义;两者都比controllers更准确 -
api/v1/可以存在,但前提是真有 v2 兼容需求;别为了“看起来专业”提前分版本 - 每个 handler 文件只负责一个资源或行为:比如
user_handler.go处理/users相关请求,不掺杂 auth 或 file 逻辑;参数校验、响应构造在此完成,但不碰db.QueryRow()
最常被忽略的一点:所有数据库连接池参数(MaxOpenConns、MaxIdleConns)必须在 sql.Open() 返回 *sql.DB 后立刻设置,而不是等第一次 db.Query() 才生效 —— 这个时机错位,线上扛不住流量时才会暴露。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










