发布于2026-07-07 阅读(0)
扫一扫,手机访问
很多团队把微服务拆完就扔那儿了,以为大功告成。结果呢?半年后,一个简单的字段变更要改五个包,测试跑一次跟开盲盒一样——这种崩塌,往往是从三个不起眼的地方开始的:接口定义模糊、包职责交叉、错误传播不透明。
先说接口。如果看到 GetUserByID、CreateOrderDB 这类命名,基本可以判断,业务逻辑已经被 HTTP 动词和数据库操作绑架了。哪天从 REST 切到 gRPC,或者从 PostgreSQL 换到 DynamoDB,所有接口签名和调用方都得重写,这代价可太大了。
正确的做法是,接口按用例命名——比如 FindUser、PlaceOrder,它表达的是“谁在什么场景下要什么结果”。参数应该用领域对象,比如 UserFilter、OrderRequest,而不是原始的 ID 或 map[string]interface{}。返回值统一用 error 表示失败,别混着用 nil、0、空 slice 这些隐式约定——那都是定时冲击波。
再来看看包的划分。光是看到 handler、service、repository 这三个包平级地放在同一个目录下,几乎就能预见到未来半年的痛苦:改一个字段,要改五个包。这是典型的按技术分层,不是按业务划分。
更合理的做法是,每个业务能力(比如 payment、inventory)独占一个包,内部自行组织 domain、transport、data 这些子目录。跨包依赖只允许“上层”引用“下层”,而且必须通过接口(比如 payment.DataStore),不能直接 import inventory.db。如果项目再大一点,go.mod 里每个业务包最好有独立的 module path,比如 github.com/org/payment,便于后期拆库或版本隔离。
错误处理也是重灾区。所有错误都用 errors.New("failed to process payment"),调用方根本分不清是风控拦截、余额不足还是网络超时。最后只能靠字符串匹配做降级,脆弱得不行,而且没法测。
更好的方案是,定义领域错误类型,比如 ErrInsufficientBalance、ErrPaymentDeclined,并实现 Is 方法。底层错误(比如数据库超时)用 fmt.Errorf("failed to persist: %w", err) 包装,保留原始 error 链。在 HTTP handler 里,统一用 errors.Is(err, payment.ErrInsufficientBalance) 来返回对应的 status code,而不是靠字符串判断。
最后是测试。最怕的是写 TestPaymentService_ProcessPayment 然后断言内部调用了 db.Sa ve()——这等于把测试绑死在了当前实现上。哪天换成事件驱动,这个测试立刻废掉。
测试的目标应该是接口契约:给定 PlaceOrderRequest,是否返回 OrderPlaced 或特定错误。可以用 testify/mock 或接口桩来替换依赖,但桩只模拟输入输出,不验证调用次数或顺序。关键路径必须包含“失败流”测试——比如支付服务里故意注入 ErrInsufficientBalance,验证上游能否收到结构化错误响应。
这里有个容易被忽视的原则:领域接口的变更成本,远低于数据结构的变更。宁可多写几个小接口(比如 Notifier、Validator),也别让一个 PaymentService 接口承担校验、记账、通知的全部职责。职责拆得越细,单测越稳,重构也越轻。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8