Go语言没有标准支付网关模块,因微信、支付宝、Stripe等网关在协议格式、签名机制、字段命名等方面差异巨大,强行抽象接口会牺牲类型安全或增加冗余;推荐按网关分包封装并共享工具函数。

Go 语言里没有“标准支付网关模块”这回事——net/http 和 encoding/json 是底层支撑,但支付逻辑、签名规则、回调验签、异步重试这些,必须按具体网关(微信、支付宝、Stripe)逐个封装,不能靠抽象接口一统江湖。
为什么不能写一个通用 PaymentGateway 接口?
表面看,Pay()、Refund()、NotifyVerify() 像能抽象,但实际调用时:微信用 body 传 XML + RSA 签名 + sign_type=HMAC-SHA256;支付宝用 application/x-www-form-urlencoded + RSA2 签名 + charset=utf-8;Stripe 直接用 Bearer Token + JSON body + 不需要业务层签名。字段名、加签时机、验签方式、错误码结构全都不一致。
强行统一接口,要么把所有参数塞进 map[string]interface{},丧失类型安全;要么为每个字段加 WxAppId、AlipayAppId、StripeApiKey 等冗余字段,调用方得自己判断填哪个——这比直接用各自 SDK 更难维护。
推荐的封装结构:按网关分包 + 共享基础能力
把微信、支付宝、Stripe 分别建包:payment/wxpay、payment/alipay、payment/stripe,每个包暴露干净的结构体和方法:
立即学习“go语言免费学习笔记(深入)”;
-
wxpay.Client持有appid、mch_id、api_key、certPath,client.UnifiedOrder()返回*wxpay.PayResp -
alipay.Client持有appID、privateKey、publicKey,client.TradePay()返回*alipay.TradePayRsp - 共用的工具函数放在
payment/util:比如util.SignSHA256WithRSA()、util.ParseFormToMap()、util.VerifyCallbackSign()
不要试图在上层再包一层 payment.Gateway 接口——当你要支持新网关时,新增一个包即可,老代码完全不受影响。
回调处理最容易翻车的三个点
支付成功后,网关会 POST 到你配置的 notify_url,这个环节出错基本等于丢订单:
- 微信回调是
application/xml,且 body 可能含 BOM 头,io.ReadAll(r.Body)后要先bytes.TrimPrefix(data, []byte("\xef\xbb\xbf")) - 支付宝回调是
application/x-www-form-urlencoded,但字段里混着中文和特殊符号,必须用r.PostFormValue("notify_id")而不是r.FormValue()(后者会自动解码两次) - 所有回调都必须做幂等校验:
SELECT FOR UPDATE查单据状态,若已是 success 则直接返回 success,不重复发货、不重复扣库存
别信“验签通过就万事大吉”——验签只保证请求来自网关,不保证没重放、没重复。
签名和证书管理别硬编码
私钥、公钥、API 密钥、证书路径这些敏感信息,绝不能写死在代码里:
- 从环境变量读:
os.Getenv("WX_PAY_API_KEY"),启动时校验非空 - 证书文件路径用绝对路径或基于
runtime.GOROOT()构造,避免因工作目录不同导致open cert.pem: no such file - 微信的
apiclient_cert.p12需用pkcs12.DecodeChain()解析,不是直接ioutil.ReadFile——否则http.Client.Transport.TLSClientConfig.Certificates会静默失败
本地调试时,用微信/支付宝沙箱环境 + 自签名证书,但上线前务必换成正式证书,且确保服务器时间与 NTP 同步——微信签名里含 timeStamp,偏差超 15 分钟直接拒单。


















