微服务可维护性崩塌源于接口定义模糊、包职责交叉、错误传播不透明三处。接口应按用例命名(如finduser)、参数用领域对象、返回统一error;包按业务能力划分并隔离依赖;错误需分类定义并包装传递;测试聚焦契约而非实现路径。

微服务不是把单体拆成一堆 main.go 就完事——可维护性崩塌往往发生在接口定义模糊、包职责交叉、错误传播不透明这三处。
接口定义必须绑定领域行为,而非 HTTP 动词或数据库操作
常见错误是写 GetUserByID、CreateOrderDB 这类名字,导致业务逻辑被协议和存储细节绑架。一旦从 REST 换成 gRPC,或从 PostgreSQL 切到 DynamoDB,接口签名和调用方全得重写。
- 正确做法:按用例命名,如
FindUser、PlaceOrder,让接口表达「谁在什么场景下要什么结果」 - 接口参数应为领域对象(
UserFilter、OrderRequest),而非原始 ID 或 map[string]interface{} - 返回值统一用
error表达失败,避免混用nil、0、空 slice 等隐式约定
包边界必须按业务能力划分,禁止按技术分层
看到 handler、service、repository 三个包平级放在同一目录下,基本可以判定未来半年会陷入修改一个字段要改五个包的泥潭。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 每个业务能力(如
payment、inventory)应独占一个包,内部自行组织domain、transport、data子目录 - 跨包依赖只允许「上层」引用「下层」,且必须通过接口(如
payment.DataStore),禁止直接 importinventory.db -
go.mod中每个业务包应有独立 module path(如github.com/org/payment),便于后期拆库或版本隔离
错误处理需显式分类,拒绝泛化 errors.New
所有错误都用 errors.New("failed to process payment"),会导致调用方无法区分是风控拦截、余额不足还是网络超时——最终只能靠字符串匹配做降级,脆弱且不可测。
- 定义领域错误类型,如
ErrInsufficientBalance、ErrPaymentDeclined,并实现Is方法 - 底层错误(如 DB timeout)用
fmt.Errorf("failed to persist: %w", err)包装,保留原始 error 链 - HTTP handler 中统一用 switch 判断
errors.Is(err, payment.ErrInsufficientBalance)返回对应 status code,不靠字符串判断
测试必须覆盖领域契约,而非实现路径
写 TestPaymentService_ProcessPayment 并断言内部调用了 db.Save(),等于把测试绑死在当前实现;换用事件驱动后,这个测试立刻失效。
- 测试目标应是接口契约:给定
PlaceOrderRequest,是否返回OrderPlaced或特定错误 - 用
testify/mock或接口桩(stub)替换依赖,但桩本身只模拟输入输出,不验证调用次数或顺序 - 关键路径必须含「失败流」测试,例如支付服务中故意注入
ErrInsufficientBalance,验证上游是否收到结构化错误响应
最易被忽略的是:领域接口的变更成本远低于数据结构变更。宁可多写几个小接口(Notifier、Validator),也不要让一个 PaymentService 接口承担校验、记账、通知全部职责——职责越细,单测越稳,重构越轻。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










