Go 项目如何组织:DDD 4 层 vs MVC vs 脚本式
从上一篇的实践出发,我们获得了这样一个基础代码结构:

my-first-agent/
├── go.mod
└── main.go ← 所有东西都在这里
虽然能够稳定运行且结构清晰,但这种做法实际上就是“脚本式”模式——没有层次划分,所有代码集中于一个文件。
Go 项目怎么组织:DDD 4 层 vs MVC vs 脚本式
读完这篇你会知道
从你现在的代码开始
阶段一:脚本式(1-3 个文件)
适合: demo、原型验证、学习用途
my-agent/
├── go.mod
├── main.go ← Agent 组装 + 问答循环
├── tools.go ← 所有工具函数
└── prompts.go ← 系统提示字符串
当你的 Agent 功能简单、无需测试、无需多人协作时,这个结构是合理的。不要过早分层。
脚本式架构开始暴露缺陷的几个征兆:
- "我想换 DeepSeek 为 Claude,但模型初始化散在 3 个地方"
- "我想给工具加单元测试,但工具直接依赖 HTTP client,没法 mock"
- "同事接手这个项目,看不懂 main.go 里的 500 行"
阶段二:MVC 三层(Web 项目的默认选择)
许多 Go 开发者因 Web 服务背景,会自然想到套用 MVC:
my-agent/
├── controller/ ← HTTP handler,接收请求
├── service/ ← 业务逻辑
└── repository/ ← 数据库操作
MVC 的核心思路是:请求处理 / 业务逻辑 / 数据存储 分开。
对 Agent 项目,MVC 能解决一半问题。 你可以把 Agent 组装放在 service/,HTTP 接口放在 controller/。但很快会遇到问题:
问题 1:模型和工具应该放哪?
service/agent.go 里直接 import openai,当你想换 DeepSeek 时,需要改这个文件。当你想测试 service/ 里的业务逻辑时,必须有一个真实的 LLM 网络连接。
问题 2:领域概念的归属不清晰。
Session(对话会话)、AgentConfig(Agent 配置)、Tool(工具)——这些"是什么"的概念,应该跟"怎么存"和"怎么传输"的代码彻底分开。MVC 没有给"概念层"留位置。
阶段三:DDD 4 层(复杂 Agent 的合理选择)
DDD(领域驱动设计)把代码分成 4 层,从内到外依赖方向是单向的:
domain/ ← 业务概念:Session、Message、AgentConfig
└── 不依赖任何外部库,纯 Go 结构体 + 接口application/ ← 用例:RunTurn、ResumeInterrupt、GetSession
└── 只依赖 domain + port 接口,不知道 HTTP/gRPC/数据库infrastructure/ ← 技术实现:数据库、LLM client、SSE 推流
└── 实现 domain 里定义的接口interfaces/ ← 协议适配:HTTP handler、gRPC server
└── 只调 application 用例,不写业务逻辑
依赖方向永远是:interfaces → application → domain,infrastructure 实现 domain 定义的接口(依赖倒置)。
具体例子:换 LLM 只改一个文件
脚本式(痛点)
// main.go
cm, _ := openai.NewChatModel(ctx, &openai.ChatModelConfig{
BaseURL: "https://api.deepseek.com",
Model: "deepseek-chat",
APIKey: os.Getenv("DEEPSEEK_API_KEY"),
})
// 想换 Claude?搜遍整个项目
DDD 4 层(怎么处理)
domain 层(定义接口,不知道有哪些模型):
// domain/port/ports.go
type LLMClient interface {
Chat(ctx context.Context, messages []Message) (string, error)
ChatStream(ctx context.Context, messages []Message) (io.Reader, error)
}
infrastructure 层(具体实现,每个模型一个文件):
// infrastructure/llmclient/deepseek.go
type DeepSeekClient struct { ... }
func (c *DeepSeekClient) Chat(...) { ... } // 实现 LLMClient 接口// infrastructure/llmclient/claude.go
type ClaudeClient struct { ... }
func (c *ClaudeClient) Chat(...) { ... }
application 层(用例,只知道 LLMClient 接口):
// application/command/run_turn.go
type RunTurnHandler struct {
llm port.LLMClient // 接口,不是具体类型
}
func (h *RunTurnHandler) Handle(ctx context.Context, ...) {
// 调 h.llm.Chat(...),不知道是 DeepSeek 还是 Claude
}
换模型只改 main.go 里的 wire(依赖注入):
// 换前
handler := &RunTurnHandler{llm: deepseek.New(...)}
// 换后
handler := &RunTurnHandler{llm: claude.New(...)}
application/ 的代码一行不动,单元测试也不需要修改。
什么时候真的需要 DDD 4 层?
诚实说:大多数个人项目和小团队用不上完整的 DDD。
DDD 的价值体现在:
| 场景 | DDD 的帮助 |
|---|---|
| 需要单元测试 Agent 逻辑 | domain/application 层零外部依赖,mock 接口就够 |
| 多人协作,边界要清晰 | 每层职责明确,不会互相踩 |
| 需要频繁换 LLM 或工具实现 | infrastructure 层可以独立替换 |
| 同一套业务逻辑要支持 REST + gRPC | application 层不感知协议 |
| 项目规模 > 3 个 BC、10+ 开发者 | 强制分层防止代码腐化 |
如果你的项目是: 1 个人、1 个 Agent 功能、不需要多协议、代码量 < 2000 行——用脚本式或者简单的 service 包分拆就够了。
实用的渐进路径
第 1 周:单文件 main.go(上一篇 的方式)
↓ 痛点出现(测试难/换模型烦)时
第 2 阶段:按职责拆文件
main.go ← 组装 + 入口
agent.go ← Agent 逻辑
tools.go ← 工具定义
model.go ← ChatModel 初始化
↓ 多人协作 / 多协议 / 持续迭代时
第 3 阶段:DDD 4 层
domain/ ← 概念
application/ ← 用例
infrastructure/ ← 技术
interfaces/ ← 协议
不要在第 1 周就上 DDD。 在你还不清楚 Agent 业务的边界时,提前分层只会让你在"这个函数该放哪里"上浪费时间。
小结
| 结构 | 适用场景 | 痛点 |
|---|---|---|
| 脚本式 | 学习、demo、原型 | 规模稍大就混乱 |
| MVC | Web 服务背景、中等规模 | 领域概念归属模糊 |
| DDD 4 层 | 多人、多协议、长期维护 | 前期分层开销高 |
下一篇讲 Prompt 模板管理:怎么从硬编码字符串升级到可版本化、可测试的模板。
综上所述,项目结构的选择应始终匹配当前规模:脚本式适合快速原型,MVC适用于中等复杂度,而DDD 4层更适合长期演进的复杂系统。避免过早优化,灵活演进是关键。