真正的防腐层必须切断外部数据结构、错误类型和行为语义对 domain 的渗透,需通过自定义 DTO、统一错误转换、最小依赖适配器及接口抽象实现完全隔离。

真正的防腐层不是加个包装函数就完事,它必须切断外部数据结构、错误类型、行为语义对 domain 的渗透。否则一次 SDK 升级或 HTTP 状态码变更,就能让核心逻辑 panic 或误判。
为什么直接用第三方 struct 和 error 是危险的
很多团队在 infrastructure/payment 包里直接 import stripe-go,然后把 stripe.Charge 当作返回值传给 usecase。这会导致:
- JSON 字段名变更(比如
paid→status)会让json.Unmarshal失败,错误直接透出到 domain 层 -
stripe.ErrInvalidRequest这类 error 被上层当成业务错误处理,而它其实该被转成你定义的ErrPaymentInvalid - HTTP status code 如
429 Too Many Requests被当普通失败处理,没走重试/降级逻辑
struct 和 error 必须彻底解耦
所有外部响应进入 domain 前,必须完成「零残留」转换:
- 定义自己的 DTO:比如
type PaymentResult struct { ID string; Status string; Amount int64 },不用任何第三方字段名或嵌套结构 - 解析时用
json.Unmarshal到你自己的 struct,而不是stripe.Charge - 错误统一用
errors.As(err, &stripe.Error{})捕获,再转成domain.ErrPaymentTimeout或domain.ErrPaymentDeclined - 禁止在 adapter 里写
if resp.StatusCode == 404 { return nil, nil }—— 这是 usecase 的职责
适配器 struct 要持有最小依赖 + 统一出口
别让 StripeAdapter 持有 *stripe.Client,那会锁死 transport、签名、重试等全部细节。正确姿势是:
立即学习“go语言免费学习笔记(深入)”;
- 只依赖
*http.Client或封装后的doRequest(ctx, req *http.Request) (*http.Response, error) - 所有请求路径都走这个
doRequest,便于测试打桩、注入日志、添加 trace header - 接口定义放在
domain/层,比如type PaymentService interface { Charge(ctx context.Context, req *PaymentReq) (*PaymentResult, error) } -
infrastructure/stripe实现该接口,但绝不暴露stripe.相关类型到上层
重试、熔断、超时不属于防腐层
这些是 cross-cutting concern,混进 adapter 会让它变重、难测、不可复用:
- 超时必须由 adapter 自己控制:
ctx, cancel := context.WithTimeout(ctx, 5*time.Second),不能靠上层 usecase 传进来一个“可能已过期”的 ctx - 重试策略(指数退避?最大 3 次?)应由单独的
retry.Retryer封装,adapter 只负责单次调用 - 熔断开关(如
breaker.Do)应放在 service wrapper 层,比如 go-zero 的namedService,而非塞进StripeAdapter.Charge
最常被忽略的一点:防腐层的边界不是“有没有中间包”,而是“domain 层代码能否在不改一行的情况下,把 Stripe 换成 Alipay 或本地模拟实现”。只要还 import 了任何第三方类型或 error,就还没真正隔离成功。


















