本节摘要:项目结构是协作的接口:目录即部门,包边界即职责边界。本节给出 Go 社区通行的标准布局,讲清 cmd 与 internal 的分工、包粒度的决策方法,以及 monorepo 与多仓库的取舍。学完本节,新同事拿到你的仓库十分钟内能开工,而不是一周。
上一章收尾时留了话:守则的终点是模式。工程侧同理——结构的终点是标准。接手过一个目录结构随缘的仓库就懂:找不到入口、分不清主次、改一处牵全身。本节从接手场景出发,把"组织架构"一次理清,这是本章后续测试与部署的地基。
设想接手一个中型并发服务:网关加任务处理,代码量数万行。前十分钟能否回答四个问题——入口在哪、领域逻辑在哪、公共工具在哪、测试怎么跑——几乎完全取决于目录结构。Go 社区沉淀了一套事实标准,让它能回答这四个问题:
shiftcenter/ ├── cmd/ │ └── gateway/ # 入口薄壳:main 函数只做装配与启动 │ └── main.go ├── internal/ # 私有代码:编译器禁止外部模块导入 │ ├── scheduler/ # 领域逻辑:调度器核心 │ │ ├── pool.go │ │ └── pool_test.go │ └── dispatch/ # 领域逻辑:分发规则 ├── pkg/ # 可对外复用的库(可选目录) │ └── ratelimit/ ├── configs/ # 配置样例 ├── test/ # 端到端与集成测试 ├── go.mod # 模块声明 └── README.md
三个目录的分工是骨架:cmd 只放入口,main 函数装配依赖后调用 internal 的启动逻辑,薄到没有 if;internal 放全部私有逻辑,按领域切分子包;pkg 可选,只有确认要对外的库才放,拿不准就不放。测试文件与被测代码同目录,这是 Go 的惯例——测试不是附件,是代码的一部分。

这张图是评审架构的尺子:依赖只许自上而下。cmd 依赖 internal 没问题;internal 里的领域包互相调用要克制成单向;pkg 与标准库被所有层使用,但绝不反向依赖任何业务层。review 时顺着一处 import 往上爬,爬出业务层的那一刻,就是架构该动刀的位置。
internal 目录的特别之处由工具链保证:其他模块无法导入你的 internal 包,写错直接编译失败。这堵墙解决一个真实痛点——你只想让同事复用,不承诺对全世界负责。项目内的各子包仍然共享 internal,墙只对外。经验法则是:默认全放 internal;某个包被第二个项目索要时,再升入 pkg 并按公共库的标准补文档与版本纪律。
新手最常见的反模式是按技术类型分包:所有模型一个包、所有工具一个包、所有工具函数一个包。结果是任何一个需求都要横跨全部包,包间依赖盘根错节。正确切口是按业务职责:调度器包拥有它自己的模型、接口与实现;分发规则包只暴露调度器需要的接口。判断粒度是否合适有一个实用信号——两个包是否总是同进同出地被改动。是,就该合并;改动总是隔离,就保持分离。
包的依赖方向要与业务方向一致:底层能力包(限流、重试、日志)不 import 任何领域包;领域包之间尽量通过接口解耦,实在要引用也保持单向。一旦两个包互相 import,编译器会直接报循环导入错误——这堵墙和 internal 的墙一样,是 Go 替架构纪律上的第一道锁。拆包时机上还有个提醒:接口定义放在消费方包里而不是实现方包里,消费方声明"我需要什么",实现方登记"我能提供什么",依赖注入才顺畅,第 2 章接口一节的隐式实现在这里再次兑现。
$ go build ./... $ go test ./internal/... $ go vet ./...
标准布局下这三条命令全项目通用:全量构建、只测内逻辑、全量静态检查。目录规范与命令行工具配合的结果是——约定越统一,自动化的门槛越低。
空谈结构不如走一遍初始化。新项目立项的头十分钟,动作固定如下:
$ mkdir shiftcenter && cd shiftcenter $ go mod init example.com/shiftcenter go: creating new go.mod: module example.com/shiftcenter $ mkdir -p cmd/gateway internal/scheduler internal/dispatch configs
四条命令之后,骨架已成:模块身份登记、三个核心目录就位。接下来写第一份代码的顺序也有讲究——先把 internal 下各包的对外接口(函数签名或接口定义)定出来,再回头填实现,最后才写 cmd 里的入口装配。自顶向下先定契约,包之间的联调成本最低;反过来从入口一路顺写到底,很容易把本该解耦的逻辑搅在一起。
目录命名还有几条小组约定值得统一:包名全小写短语(scheduler 而不是 Scheduler),不用下划线与驼峰;一个目录一个包,不搞"大杂院";文件按职责拆分而非按大小凑整(pool.go、metrics.go 各司其职),单文件超过几百行就该考虑切分。这些细节单独看都小,合起来决定了仓库的"体感整洁度"。
背景:接手的调度服务是个单体:一个 main 包一万两千行,编译尚可,协作全靠口头沟通"改了哪儿别碰我哪儿"。
操作:按职责切三刀——调度循环、分发规则、结果上报,各自成 internal 子包;包间通信只经接口与结构体传值;入口挪进 cmd。切分全程保持每一步可编译可测试,小步提交。
结果:三周后包结构稳定,调度循环与分发规则可以由不同人并行改动,测试从"整包跑一次二十分钟"变成"按包秒级反馈"。
解读:这次重构没有引入任何新功能,协作成本却显著下降——结构的本质是"把沟通协议写进目录"。切包的时机也有讲究:不到痛点不动刀,动了刀就切到职责边界为止,半途而废的切分比不切更乱。
变式:团队扩大到多服务后,跨仓库复制粘贴公共代码不可持续,此时评估 Go workspace(多模块单仓库工作区):本地开发时多个 module 联动调试,发布时各模块独立打版本。它是 monorepo 与多仓库之间的中间路线,边界判断标准依然是那句:依赖方向是否依旧自上而下。
架构立住了,下一节给代码装上安全网:测试与基准,让每一次改动都有底气。