Go语言模块化核心是职责边界+显式依赖+物理包隔离,反对server.GlobalFunc式伪命名空间;包应按单一职责划分,如internal/server仅含Server类型与生命周期,HTTP逻辑下沉至internal/handler,数据库操作收于internal/store并暴露语义化函数;跨包调用须显式导入,禁止就近引用;可复用代码放pkg/,internal/间不得引用私有类型;接口应小而具体、体现行为而非实体,优先复用标准库接口;模块边界靠go mod与目录结构双重锁定,建议按能力域拆分独立module并用replace开发。

Go 语言里没有“优雅模块”的魔法开关,只有清晰职责边界 + 显式依赖 + 物理包隔离。想靠 server.GlobalFunc() 这种伪命名空间调用,只会让代码越来越难维护。
包划分必须基于职责,而非目录层级
很多人把 internal/server/ 当成“服务层”,再塞进 handler、middleware、model,结果包内耦合严重,改个日志格式要测全包。真正的高内聚,是让一个包只做一件事,并能独立测试。
-
internal/server应只包含Server类型定义、启动/停止生命周期方法、核心状态管理——不碰 HTTP、不碰数据库 - HTTP 相关逻辑(路由、中间件、
http.Handler实现)应下沉到internal/handler,它依赖server.Server但不反向依赖 - 数据库操作收在
internal/store,只暴露SaveUser()、FindUserByID()等语义化函数,不暴露*sql.DB或 ORM 实例
跨包调用必须显式导入,禁止“就近引用”
常见错误是把通用函数扔进 cmd/daemon/common.go,然后被 internal/server 直接 import —— 这违反了 internal/ 的封装契约,外部项目无法复用该逻辑,且未来提取为独立库时要重写所有路径。
- 可复用的工具函数(如配置解析、错误码映射、时间格式化)必须放在
pkg/util或pkg/errors这类明确导出的包中 -
internal/下的包之间可以相互 import,但internal/a不得 importinternal/b的私有类型(如b.unexportedStruct),否则就是隐式耦合 - 如果
internal/server需要发 HTTP 请求,不要直接 importnet/http,而是定义HTTPClient接口,由上层注入——这样单元测试才能 mock
接口设计要小而具体,别抽象过早
看到“用户服务”就定义 UserServiceInterface,里面塞 12 个方法,这是典型抽象失败。Go 的接口价值在于组合与替换,不是为了“看起来像面向对象”。
立即学习“go语言免费学习笔记(深入)”;
- 先写具体实现(如
mysqlUserStore),等出现第二个实现(如redisUserCache)再抽接口 - 接口名体现行为,不是实体:用
UserReader、UserWriter,而不是UserRepository - 优先复用标准库接口:
io.Reader、io.Closer、fmt.Stringer,比自定义DataProvider更易理解、更易测试
模块边界要靠 go mod 和目录结构双重锁定
go.mod 不只是记录依赖,它是模块可见性的开关。很多团队把整个项目当一个 module,导致 pkg/util 被任意 internal/ 包随意修改,破坏稳定性。
- 生产项目建议按能力域拆 module:比如
github.com/yourorg/auth、github.com/yourorg/logging,每个 module 有独立go.mod和版本号 - 主项目
go.mod中通过replace指向本地路径开发,上线前切回 tagged release - CI 流程中加一条检查:
grep -r "github.com/yourorg/" internal/ | grep -v "pkg/",命中即报错——防止内部包偷偷引用外部 module
最常被忽略的一点:高内聚不是靠命名或文档维持的,是靠 go build ./... 失败来守住的。一旦某个包开始 import 它本不该知道的类型或路径,编译就会报错——这才是 Go 模块化的真正护栏。


















