工程化核心是清晰分层与边界管控:cmd/只负责启动,internal/封装业务逻辑,api/暴露契约,测试用_test包验证,依赖由go mod tidy自动管理。

直接上手就建 main.go 然后往里堆逻辑?项目一过千行就开始改不动、不敢测、不敢发——这不是你写得慢,是结构没立住。工程化不是加目录套壳,而是让每个文件知道“它该管什么、不该碰什么”。
go mod init 后第一件事:关掉 GOPATH 依赖惯性
很多人 go mod init 完,还在 $GOPATH/src 下建项目,或手动把第三方包 go get 到 src 里。这会导致:
-
go build行为不一致(本地能跑,CI 上报import not found) - 版本锁定失效(
go.mod里写了v1.9.1,但src里实际是master分支) -
go list -m all查不到真实依赖树,排查冲突时抓瞎
正确做法:
- 确保
GO111MODULE=on(用go env -w GO111MODULE=on永久设置) - 项目根目录外不放任何
.go文件;所有依赖只通过go mod tidy自动拉取 - 删掉
$GOPATH/src下的手动 clone 仓库(除非你明确在做 Go 工具链开发)
目录结构别抄模板,先划清「谁负责启动、谁负责干活」
看到 golang-standards/project-layout 就照搬?容易卡在“该放 internal/user 还是 pkg/user”上。其实只要守住两条线:
立即学习“go语言免费学习笔记(深入)”;
-
cmd/下只放极简main.go:只做初始化依赖、调用入口函数,**不写业务逻辑、不 import 业务包以外的第三方库** -
internal/是你的“私有领地”:所有业务逻辑、领域模型、数据访问层全放这儿,外部模块(包括测试)不能 importinternal/xxx
示例:cmd/myservice/main.go 只有:
func main() {
cfg := configs.Load()
db := database.New(cfg.DB)
srv := server.New(db, logger.New())
srv.Run(cfg.Addr)
}
——所有 configs、database、server 都是 internal/ 下的包,cmd 层绝不越界。
接口定义别藏在 internal 里,暴露给调用方的契约要独立
常见错误:把 HTTP handler 的请求/响应结构体、RPC 方法定义全塞进 internal/api,结果前端 SDK、其他服务调用方只能硬读 Go 源码,改个字段就得同步所有人。
真正需要隔离的是实现,不是契约。建议:
- 新建
api/目录(与internal平级),放openapi.yaml或proto/文件 - 用
oapi-codegen或protoc-gen-go自动生成 client/server stub,而不是手写 struct -
internal/handler只 importapi生成的类型,不自己定义type UserReq struct
这样改 API 时,只需更新 api/ 下的定义,重新生成即可,业务逻辑层完全无感。
测试路径别绕开 internal 的包可见性限制
想测 internal/user/service.go 里的函数?别为了测试把包挪到 pkg,也别加 //go:build ignore 注释绕过。Go 原生支持 _test 包名:
- 在
internal/user/下建service_test.go,package 名写成user_test - 这个文件可以 import
user包里的所有非小写首字母标识符(即非私有成员) - 同时也能 import 其他
internal/xxx包(比如internal/database),方便集成测试
关键点:不要在 user_test 里写业务逻辑,只写验证逻辑;所有被测代码仍在 user 包内,边界清晰。
最常被跳过的其实是 cmd/ 和 internal/ 之间的那条线——一旦 main.go 开始调用 user.Create() 而不是 srv.Run(),整个分层就塌了。工程化不是目录多,是每层都守得住自己的边界。


















