Go 里调用外部 API 到底该怎么写?第三种方式真香。。

发布时间:2026/8/22 16:40:58
Go 里调用外部 API 到底该怎么写?第三种方式真香。。 第一次写 Go 服务的时候我特别自信。不就是调个支付接口吗http.Post一把梭搞定。代码能跑测试能过上线也没出啥问题。我当时觉得这有什么好纠结的。直到三个月后产品说“我们要换一家支付服务商。”那一刻我才发现我的代码里http.Post散落在十几个文件里每个地方都在拼 URL、设 Header、解析响应。换一家支付商意味着我要把这十几个地方全找出来一个个改。而且因为每家 API 的请求格式、字段名、错误码都不一样我几乎是在重写整个支付模块。那是我第一次意识到调用外部 API 这件事如果不在第一天把它设计好后面一定会加倍还回来。后来我试过三种不同的写法。第一种很自然但最后把自己写进了死胡同。第二种干净了一点但抽象过了头。第三种——也是我现在唯一会用的方式——终于让我觉得“对了”。下面我把这三种写法和它们的翻车时刻都讲一遍。写法一把 HTTP 逻辑包成一个结构体这是大多数人本能会做的第一步。把调用同一家外部服务的逻辑集中到一个 struct 里注入一个http.Client完事。typePaymentClientstruct{baseURLstringapiKeystringhttpClient*http.Client}funcNewPaymentClient(baseURL,apiKeystring)*PaymentClient{returnPaymentClient{baseURL:baseURL,apiKey:apiKey,httpClient:http.Client{Timeout:10*time.Second},}}func(c*PaymentClient)Charge(ctx context.Context,orderIDint,amountstring)error{// 拼 URL、设 Header、发请求、解析响应// ... 大概 30 行代码}比直接http.Post好多了。HTTP 逻辑集中了可以 mock 了超时和 base URL 也能配置了。什么时候翻车typeOrderServicestruct{orders OrderRepo payment*PaymentClient// 依赖的是具体类型}问题出在这里OrderService直接依赖了*PaymentClient——一个具体类型不是一个接口。写单元测试的时候我想 mock 支付服务但我没有办法把*PaymentClient替换成一个假实现。除非我改OrderService的签名或者用一个笨重的 HTTP 测试服务器。更麻烦的是如果哪天我想换一家支付商我不仅得写新的客户端还得改OrderService的代码——但OrderService里压根没有业务逻辑变化它只是“调用支付”而已。写法二给每个客户端定义一个接口直觉告诉我用接口啊依赖接口不依赖具体类型。typePaymentClientinterface{Charge(ctx context.Context,orderIDint,amountstring)error}typeOrderServicestruct{orders OrderRepo payment PaymentClient// 依赖接口好多了}现在可以 mock 了测试好写了也可以换实现了。什么时候翻车接口的定义放在了payment包里——也就是基础设施层。而OrderService在业务层它import 了基础设施层的代码。在整洁架构Clean Architecture里依赖方向应该是业务层 ← 基础设施层而不是反过来。业务层不应该知道“有个东西叫 PaymentClient”它只应该知道“我需要处理一笔支付”。如果你的业务层 import 了payment包那哪天你想换支付商的时候依然要改OrderService的 import 语句。接口确实解耦了实现但没有解耦“支付这个概念本身”。写法三接口属于业务层而不是基础设施层这是真正解决问题的写法。它叫端口与适配器Ports and Adapters。核心思想一句话接口定义在业务层用业务语言命名由基础设施层去实现它。// domain/payment.go —— 业务层定义端口typePaymentServiceinterface{Pay(ctx context.Context,orderIDint,amount decimal.Decimal)(string,error)}这里没有Charge没有Currency没有API Key。只有业务语言“支付这笔订单返回一个参考号”。// infrastructure/stripe_adapter.go —— 基础设施层实现端口typestripeAdapterstruct{baseURLstringapiKeystringcurrencystring}func(a*stripeAdapter)Pay(ctx context.Context,orderIDint,amount decimal.Decimal)(string,error){// Stripe 的 HTTP 调用、JSON 序列化、错误处理全在这里// 返回的就是业务层需要的参考号}现在OrderService长这样typeOrderServicestruct{orders OrderRepo payment domain.PaymentService// 依赖的是业务层的接口}func(s*OrderService)ConfirmOrder(ctx context.Context,orderIDint)error{order,_:s.orders.Find(ctx,orderID)ref,err:s.payment.Pay(ctx,orderID,order.Amount)iferr!nil{returnerr}order.MarkPaid(ref)returns.orders.Update(ctx,order)}OrderService不知道 Stripe 是什么不知道 HTTP 是什么不知道货币是什么。它只知道一件事“我需要调用支付服务来确认这笔订单。”换支付商的时候我只需要写一个新的 adapter比如midtrans_adapter.go然后在启动代码里把依赖注入换掉就行——OrderService一行都不用改。错误处理别把 HTTP 状态码漏到业务层很多人写着写着就容易把 HTTP 状态码往上抛。这是不对的。业务层只关心“支付被拒了”、“支付服务挂了”、“支付成功了”这三件事。它不关心 402、503 还是 200。// 在 adapter 里做翻译switchresp.StatusCode{case402,422:return,domain.ErrPaymentDeclinedcase503,504:return,domain.ErrPaymentProviderDowncase200:// 继续解析default:return,fmt.Errorf(unexpected status: %d,resp.StatusCode)}业务层拿到的是domain.ErrPaymentDeclined而不是http.StatusPaymentRequired。这样它才能做有意义的业务决策。测试终于不用搭 HTTP 服务器了因为OrderService只依赖接口测试的时候就很简单了typemockPaymentstruct{payFuncfunc(orderIDint,amount decimal.Decimal)(string,error)}func(m*mockPayment)Pay(ctx context.Context,orderIDint,amount decimal.Decimal)(string,error){returnm.payFunc(orderID,amount)}funcTestOrderService_PaymentFailed(t*testing.T){svc:OrderService{payment:mockPayment{payFunc:func(_,_)(string,error){return,domain.ErrPaymentDeclined},},}err:svc.ConfirmOrder(ctx,123)if!errors.Is(err,domain.ErrPaymentDeclined){t.Fatal(expected declined error)}}不需要启动一个假的 HTTP 服务器不需要设置环境变量不需要担心网络超时。纯粹的业务逻辑测试跑得飞快。总结三种写法三代进化写法优点痛点封装成 struct逻辑集中能配置业务层依赖具体类型换实现得改业务代码接口放基础设施层能 mock解耦实现业务层还是知道“支付客户端”的存在依赖方向不对接口放业务层业务层干干净净换实现不改业务代码需要多想一步“接口应该属于谁”第三种写法最大的好处不是“技术正确”而是让你在半年后换支付商的时候不用在十几个文件里搜索http.Post然后挨个改。这种“不给自己留坑”的设计才是一个 Go 服务能持续活下去的原因。