go微服务项目必须严格区分internal(私有业务逻辑,仅本项目使用)与pkg(公共可复用库,支持独立go get),cmd仅含main函数负责初始化和启动,接口定义须置于被依赖方包内以避免循环依赖,gofmt需强制纳入ci防线。

Go 微服务项目一旦多人协作、迭代变多,代码混乱不是“会不会发生”的问题,而是“什么时候爆发”的问题。规范和工程化不是锦上添花,是防止 internal 目录变成“internal 乱炖”的底线。
go.mod 里依赖太多?先搞清哪些该进 internal,哪些该进 pkg
常见错误:把所有工具函数、HTTP 客户端、日志封装全塞进 internal/utils,结果其他服务想复用时只能复制粘贴或硬拆包。
-
internal/是私有域:只供本项目cmd/启动的二进制使用,外部无法 import —— 所有业务逻辑、handler、service、repository 都该放这里 -
pkg/是公共出口:比如pkg/logger、pkg/httpclient,必须能被其他项目独立go get,且不依赖本项目任何internal包 - 如果某个“工具”只被一个 service 用,它就该在
internal/service/xxx/util.go里,而不是提成全局utils
接口定义写在哪?别让 handler 直接调 service 实现
现象:修改一个数据库字段,要改 handler → service → repository 三层里的类型定义,还漏掉测试桩。
- 接口定义必须放在被依赖方的包里:比如
internal/user/service.go定义UserService接口,internal/user/repository.go定义UserRepository接口 - 实现结构体放在同一包或子包(如
internal/user/mysql_repository.go),但构造函数返回接口类型,而非具体 struct - 避免跨包定义接口:比如在
internal/handler里定义UserService接口,会导致service包反过来依赖handler,形成循环
gofmt 不只是格式化,它是 CI 里第一道防线
很多团队把 gofmt -w 当成开发后手动执行的“美化操作”,结果 PR 里混着不同缩进、括号换行风格,Code Review 变成找空格游戏。
- CI 流程中必须加
gofmt -l ./ | grep .:有输出就失败,强制所有人提交前跑gofmt -w - 别用
goimports替代gofmt做基础格式检查——它可能误删未使用的 import,而gofmt只动格式,不动语义 -
gofmt -r谨慎用于重构:比如统一替换fmt.Printf→log.Printf,但必须本地验证 AST 是否真匹配,否则可能把字符串里的Printf也替了
配置加载失败时 panic 还是返回 error?看启动阶段
错误写法:viper.ReadInConfig() 失败直接 log.Fatal,导致测试时无法 mock 配置源,或者容器里 config.yaml 缺失却打出一屏 panic 栈。
- 应用启动入口(
cmd/xxx/main.go)可以panic:因为此时没别的路可走,不加载配置就无法继续 - 但任何可复用的模块(比如
pkg/config或internal/db的初始化函数)必须返回error,由上层决定是否终止 - Viper 的
AutomaticEnv()和SetEnvPrefix()要早于ReadInConfig()调用,否则环境变量不会 fallback 到文件缺失时
真正难的不是记住目录名或接口命名规则,而是每次加新功能时,下意识判断:这个东西,三个月后别人接手,能不能一眼看出它属于哪一层、被谁依赖、改了会影响什么范围。工程化不是堆结构,是让结构自己说话。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











