Go 项目结构与分层

组织 Go 项目时关键的不是背目录模板,而是包边界、依赖方向与变化隔离;cmd/internal 与分层只是实现这些原则的手段。

#type / synthesis #status / growing #tech / dev #tech / dev / backend #resource / go

[!info] 关联笔记

Go 项目结构与分层

这个概念为什么会出现

进入真实服务后,问题不再是“能不能编译”,而是:

  • 改 HTTP 协议会不会牵动数据库代码
  • 业务规则能不能在 CLI、定时任务、HTTP 间复用
  • 新同事能否在 10 分钟内找到“用例从哪进、数据从哪出”
  • 测试能否只测业务而不起整站

目录名(pkg/internal/domain/)只是可见的结果。真正要设计的是:包边界、依赖方向、变化来源。很多人先抄一套“标准布局”,结果小项目被脚手架压垮,大项目却仍循环依赖。

[!abstract] 一句话理解 先按职责与依赖方向切包,再选目录:接口层依赖业务,业务不依赖具体框架/DB 驱动细节;cmd 组装、internal 封装、测试与部署围绕边界展开。

最小可运行示例

先把示例放进业务场景,再看代码:

场景:订单服务骨架——cmd 组装,用例依赖仓储接口

新开一个订单微服务。小服务不必上 DDD 全家桶,但要先定依赖方向

  • cmd 只组装
  • HTTP 调用例
  • 用例依赖 OrderRepo 接口,不绑 Postgres

文件可合并;关键是箭头单向。

myapp/
  go.mod
  cmd/myapp/main.go          # 组装:读配置、建依赖、起服务
  internal/
    app/order/               # 用例:创建订单、取消订单
      service.go
    domain/order/            # 领域模型与规则(可选独立)
      order.go
    httpapi/                 # 传输:HTTP 路由与 DTO
      handler.go
      routes.go
    store/postgres/          # 基础设施:实现仓储接口
      order_repo.go
package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"time"
)

// OrderRepo:仓储端口——用例不关心 postgres 细节
type OrderRepo interface {
	Save(ctx context.Context, id string) error
}

// OrderService:创建订单用例
type OrderService struct {
	Repo OrderRepo
}

func (s *OrderService) Create(ctx context.Context, id string) error {
	// 领域规则:id 必填(可再扩展库存、幂等等)
	if id == "" {
		return fmt.Errorf("empty id")
	}
	return s.Repo.Save(ctx, id)
}

// memRepo:演示用内存实现;生产换成 store/postgres
type memRepo struct{}

func (memRepo) Save(ctx context.Context, id string) error {
	// 尊重取消:客户端断开则不必继续写
	select {
	case <-ctx.Done():
		return ctx.Err()
	default:
		_ = id
		return nil
	}
}

func main() {
	// main = 组装根:接线,不写业务分支
	svc := &OrderService{Repo: memRepo{}}
	mux := http.NewServeMux()
	// 传输层:解码参数 → 调用例 → 映射状态码
	mux.HandleFunc("POST /orders", func(w http.ResponseWriter, r *http.Request) {
		id := r.URL.Query().Get("id")
		if err := svc.Create(r.Context(), id); err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		w.WriteHeader(http.StatusCreated)
	})

	srv := &http.Server{
		Addr:              ":8080",
		Handler:           mux,
		ReadHeaderTimeout: 5 * time.Second,
	}
	log.Fatal(srv.ListenAndServe())
}

建议运行:

go run .
# 终端 2:
curl -s -o /dev/null -w "%{http_code}\n" -X POST "http://localhost:8080/orders?id=o1"
curl -s -o /dev/null -w "%{http_code}\n" -X POST "http://localhost:8080/orders"

期望:

201
400

结合场景再看三个关注点

  1. main 只做组装;业务不 import 具体框架全局。
  2. OrderService 依赖 OrderRepo 接口,存储可替换。
  3. 包名表达职责(order、httpapi),不是技术时髦词堆砌。

核心概念与准确模型

1. 包是边界,目录是投影

Go 的编译与可见性以为单位(见 包与可见性)。好结构首先回答:

问题好答案的特征
这个包对外承诺什么?少而稳的导出 API
谁可以依赖谁?单向、无环
变化从哪进来?协议、DB、第三方 SDK 被边界挡住
测试如何站在边界外?对接口打桩,不必起全栈

目录只是让人眼与 go list 舒服的投影。

2. 常见分层(逻辑层,不是强制文件夹)

flowchart TB
    T["传输层 HTTP/RPC/CLI"] --> A["应用/用例层"]
    A --> D["领域规则与模型"]
    A --> P["端口/接口"]
    I["基础设施 DB/MQ/HTTP Client"] --> P
职责典型依赖
传输编解码、状态码、鉴权中间件用例 API、DTO
应用/用例事务边界、编排、权限检查领域、仓储接口
领域不变量、状态迁移尽量零基础设施
基础设施SQL、SDK、文件系统实现端口接口

小项目可把“领域 + 用例”合在一个 internal/app/foo 包;不要为了分层而分层

3. 社区惯例目录的真实含义

路径含义误用
cmd/<name>/可执行入口,薄 main塞满业务
internal/仅本 module 可导入(编译器强制)把一切塞进一个 internal/pkg 大杂烩
pkg/有意对外库式 API(可选)假装“更专业”而空转
api/OpenAPI/proto 等契约源与实现循环引用
configs/deploy/scripts/运维与工具运行时强依赖未嵌入的路径(见 [[go-embed

官方并未规定唯一标准树;Organizing a Go module 强调 module 内清晰与 internal 封装。golang-standards/project-layout 是社区模板,不是语言规范。

4. 依赖方向规则

  1. 稳定方向内指:框架适配器 → 用例 → 领域。
  2. 具体实现指向抽象postgres.OrderRepo 实现 order.Repository,而不是 service import 驱动细节到处飞。
  3. 禁止反向domain 不得 import httpapi 或某 ORM 包。
  4. 跨包共享:优先共享接口与 DTO,慎共享“什么都有的 util”。

5. 按业务切片 vs 按技术分层

方式优点风险
技术层 handlers/ services/ repos/上手快大了后跨目录改一个用例
业务竖切 order/ billing/ 内含各层用例内聚公共内核要克制
混合:internal/order 竖切 + 薄 platform/多数中型服务甜点需约定清晰

经验:边界以用例变化频率为准,不是以“目录对称美观”为准。

6. 配置、日志、遥测放哪

  • 构造期注入:LoggerMeterConfig 作为依赖传入。
  • 避免每个包 init() 读全局文件。
  • main 负责:读环境变量/文件 → 构造图 → Run(ctx)

设计动机

  1. 变化隔离
    HTTP 换 gRPC、Postgres 换别家,不应重写业务规则。
  2. 可测性
    用例对接口测试;传输层用 httptest;存储层用集成测。
  3. 团队导航
    新人按“入口 → 用例 → 存储”三条线找代码。
  4. 与 Go 工具链契合
    包级可见性、internal 强制封装、module 边界清晰(Modules)。

边界情况与反直觉行为

1. 过早微服务 / 多 module

单仓单 module 服务未稳就拆仓库,会把“函数调用”变成“分布式失败模式”。先包边界,后仓边界。

2. utils / common 黑洞

无领域语义的大包会成为循环依赖与隐性耦合中心。优先:就近私有辅助;真共享再提 internal/x

3. 接口定义在错误的一侧

若每个实现旁都定义“自己的接口”,调用方无法统一。惯用:接口由消费者定义(小接口),或放在领域端口包。

4. 把 DTO 当领域模型

JSON tag、DB tag 堆在同一结构上,传输变更撕裂持久化。见 JSON 与序列化

5. pkg/ 对外承诺过重

一旦有外部 module 依赖 pkg,演进成本上升。默认先 internal,真要开源库再提升。

常见误区

[!warning] 常见误区:先抄完整 project-layout 错误:空目录二十个,业务仍糊在 main
正确:三五个包把依赖方向做对,再随痛点加目录。

[!warning] 常见误区:分层 = 每层一个目录必须对称 错误:10 行逻辑也硬拆 domain/application/infrastructure。
正确:复杂度不够时合并;箭头正确优先。

[!warning] 常见误区:业务包 import 框架全局 错误:service 里直接依赖某 Web 框架 Context 类型。
正确:传输层适配;业务用 context.Context 与自己的类型。

[!warning] 常见误区:循环依赖用“再抽一个包”无限后移 错误:a → b → a 靠第三个糊墙。
正确:重新划分所有权或引入接口断环。

与相邻概念对比

概念差异
包与可见性语言机制;本篇是机制上的结构策略
Modules依赖版本与仓边界;layout 是仓内组织
H/S/RWeb 场景的一种具体分层落地
构建部署产物与交叉编译;入口多在 cmd/
整洁/六边形架构思想同源;Go 落地要克制仪式感

工程实践

  1. cmd 保持瘦:组装 + 信号 + 退出码。
  2. 默认 internal:除非明确要对外库。
  3. 接口小而稳Save/Find 而非上帝 Repository
  4. 一个用例一个主路径文件,复杂再拆 private 文件(同包)。
  5. 测试放同包 _testfoo_test 黑盒,按需要选。
  6. 文档化依赖图:在 README/MOC 画一张“允许 import”表。
  7. CIgo test ./...、竞态、lint import 周期(如 go vet、额外分析器)。
  8. 演进:从单体包 → 按痛点拆,而不是按设想一次拆完。
// 允许的依赖方向(约定示例)
// httpapi → app → domain
// store   → domain (实现接口)
// cmd     → 所有具体实现(唯一组装点)

可验证实验

实验 1:画依赖

对现有小项目跑 go list -f '{{.ImportPath}} {{.Imports}}' ./...,标出是否有“领域 → 传输”。

实验 2:替换存储

OrderRepo 增加 fakeRepo,不改 service 跑单测。

实验 3:第二入口

cmd/myapp-cli 调用同一 OrderService,验证业务不绑 HTTP。

实验 4:故意反向 import

让 domain import httpapi,观察团队约定/CI 是否拦得住。

本节总结

  • 本质:结构 = 包边界 + 依赖方向 + 变化隔离;目录是投影。
  • 手段cmd 组装、internal 封装、用例与基础设施分离。
  • 克制:小项目简单清晰 > 仪式化分层。
  • 下一步:用 net/http优雅关闭 填满 cmd 生命周期;用测试钉住端口。

自测题

概念题

  1. 为什么说“目录模板不是核心”?
  2. internal/ 提供了什么语言级保证?
  3. 接口应更常由谁定义?

代码推理题

package domainimport "myapp/internal/httpapi" 意味着什么架构问题?

工程思考题

10 人团队、单仓、HTTP+异步消费者共用下单逻辑,你如何切包,避免两套业务分叉?

参考答案

展开
  1. 真正约束可维护性的是依赖方向与职责边界;目录名可替换。
  2. 其他 module 无法导入该路径,封装被编译器强制。
  3. 更常由消费者(用例)定义小接口,实现放在基础设施。
    代码题:领域依赖传输,依赖反转失败,业务被 HTTP 细节污染。
    工程题:把下单用例放 internal/app/order,HTTP 与 consumer 都只做适配并调用同一 service;共享领域规则,不共享框架类型。

延伸阅读与资料来源

资料类型支撑
Organizing a Go module官方文档module 内组织建议
Package names官方博客命名与职责
Go Modules Reference规范module 边界
Standard Library参考标准库自身的包划分风格
Code Review CommentsWiki包与 API 评审习惯

笔记元信息

  • 建议文件名:go-project-layout-and-layering.md
  • 所属阶段:服务工程化
  • 学习顺序:Modules 与包可见性之后
  • 建议下一篇:分层落地优雅关闭
  • 本篇状态:已深化(结构完整;强调依赖方向而非目录教条)
创建于 2026/6/20 更新于 2026/7/15