Go模块(go.mod)仅负责依赖版本管理,不构成分层架构;真正的分层依赖包组织(如internal/约束)、接口抽象与编译器可见性规则,其中internal/是唯一由编译器强制执行的隔离机制。

Go 模块依赖本身不参与“精简内核设计”——它只是版本管理机制;真正起分层作用的是包(package)组织方式和 import 规则,模块(go.mod)只负责把它们按语义版本锁住。
为什么 go mod 不等于分层架构
很多人误以为只要用 go mod 就自动实现了分层。其实 go.mod 只干三件事:声明模块路径、记录直接依赖、校验 go.sum。它不管你的 internal/handler 能不能 import internal/gateway,也不阻止你把数据库 model 直接塞进 HTTP handler 里。
分层靠的是目录约束 + 接口抽象 + 编译器强制可见性规则,比如:
-
internal/下的包无法被外部模块 import(Go 编译器硬限制) -
domain/包不能 importdatabase/sql或gin.Context -
pkg/里的pkg/logger可以被所有层用,但不能反过来依赖internal/
internal/ 目录才是分层的实际执行者
Go 没有语言级的“层”概念,internal/ 是唯一由编译器强制 enforce 的隔离机制。它的存在让“上层不能直接调用下层实现”变成可落地的工程约束,而不是靠文档或 Code Review 约束。
立即学习“go语言免费学习笔记(深入)”;
典型错误现象:
- handler 直接 new 一个
sqlc.Querier实例 —— 违反了 gateway 层抽象职责 -
internal/user包里写了import "myapp/internal/order"—— 循环依赖或跨域调用,go build会报错 - service 方法返回
*gorm.Model—— domain 边界被击穿,后续换 ORM 时所有 handler 都得重写
正确做法是:在 usecase/ 定义 interface,让 gateway/ 去实现它,handler 只依赖 usecase 接口。
模块版本控制如何影响分层稳定性
当你把某个通用能力抽成独立 module(比如 github.com/myorg/pkg/logger),它就脱离了主项目的 internal/ 保护。此时必须确保它:
- 不引用任何
internal/或domain/类型(否则下游项目无法编译) - 错误类型用
errors.Is可识别的 wrapper,而不是具体 struct(如避免暴露user.ErrNotFound) - 配置初始化不耦合
viper或envconfig—— 用函数参数传入,而非全局 init
否则看似“复用”,实则把主项目的分层污染扩散到外部,导致升级一个 logger 版本就得同步改所有业务仓库。
真正难的不是写 go mod tidy,而是每次新增一个 import 时,下意识问一句:这个包该出现在哪一层?它会不会让某层悄悄越过边界去碰另一层的实现细节?


















