该拆包是止损而非重构,判断依据是协作信号:依赖混搭、git blame关注点分散、测试变慢且失败无关、出现import cycle;拆包应先接口隔离再目录拆分,严守跨包调用三原则,确保新包独立编译测试部署。

旧业务模块该拆了,不是因为代码行数多,而是当你改一个字段校验逻辑,得同时看 handler、service、repository 三个文件,还怕影响定时任务和导出功能——这时候拆包就不是重构,是止损。
怎么判断这个包真该拆了
别数行数,看协作信号:
-
go list -f '{{.Deps}}' ./pkg输出里反复出现database/sql、net/http、github.com/xxx/queue这三类混搭依赖 -
git blame pkg/user.go显示最近 10 次提交:3 次改 API 参数校验,4 次调 DB 事务,2 次修消息重试逻辑,1 次加导出 Excel —— 关注点完全不重叠 -
go test ./pkg耗时从 800ms 涨到 4.2s,且失败测试常和本次改动无关(比如改了密码策略,TestUserExport突然挂) - CI 上出现
import cycle not allowed: pkg → pkg/internal/event → pkg这类错误
拆包第一步:先切职责,再动目录
别一上来就建 user-core、user-api、user-job 三个新目录。先在原包内用接口隔离:
- 把所有 HTTP 相关逻辑(
http.HandlerFunc、绑定、响应封装)抽到pkg/user/api/,只保留RegisterRoutes(*mux.Router)函数 - 把数据库操作抽象成
UserRepository接口,定义在pkg/user/port/,实现留在pkg/user/infra/mysql/(但禁止其他包 import 这个路径) - 把定时任务逻辑移入
pkg/user/job/,它只依赖port.UserRepository和port.UserEventPublisher,不碰http或sql - 删掉所有
init()函数——它们常是循环依赖的温床,改用显式启动函数如job.StartScheduler()
跨包调用必须守死三条线
拆完最怕“伪解耦”:目录分开了,代码还紧绑着。关键防线:
- 接口定义必须放在**调用方包内**,比如
order包要查用户状态,就在order/port/user.go定义type UserStatusChecker interface { Check(id int) (bool, error) },让user包去实现它 - 禁止任何 struct 跨包传递:
order不得 importuser.User,只收user.UserSummary(DTO,定义在user/port下) - 数据库连接、HTTP client、Redis 实例等资源对象,只允许在
cmd/main.go或internal/app初始化并注入,各业务包只接收接口
测试必须跟着包走,否则等于没拆
旧包测试还在跑,新包没覆盖,是最常见的拆包失败现场:
- 删掉原
pkg/user/user_test.go,为每个新子包建独立测试文件:pkg/user/api/api_test.go、pkg/user/port/port_test.go -
go test -v ./user/api/必须只跑api包的测试,不能顺带执行mysql实现里的 SQL - 对
port.UserRepository的测试,用内存 mock(如map[int]*User),绝不用sql.Open();真实 DB 测试放到集成测试目录integration/user_db_test.go - 检查
go mod graph | grep user,确保没有user包意外依赖order或payment的路径
真正难的不是拆,是让每个新包能独立编译、独立测试、独立部署——如果现在 go build ./user/api/ 还报错说找不到 github.com/xxx/project/internal/db,说明接口没抽干净,或者路径引用没切对。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











