go接口应按需发现而非预先设计,核心原则是:仅当存在多实现需求(如日志/通知/支付渠道切换)或条件分支重复出现时才定义;接口须小而专注、按能力契约拆分;构造函数必须显式注入依赖,避免nil panic;测试应验证真实实现而非仅mock。

Go 接口不是“为了用而用”的装饰,而是你写代码时对“谁该知道什么”的一次主动划分。它不解决语法问题,只帮你守住边界。
接口定义前先问:这个行为是否会被不同实现替换?
很多初学者一上来就定义 type Repository interface,结果发现所有实现都只用 MySQL,连 mock 都没写过。这不是接口滥用,是抽象时机错了。
- 如果当前只有 1 种实现,且未来半年内没有切换数据库、缓存或消息队列的计划,
Repository接口可以暂缓——先让结构体直接实现业务逻辑,等第一个分支出现(比如加 Redis 缓存)再抽接口 - 真正该立刻建接口的,是那些“今天写死、明天就得改”的点:比如日志输出目标(控制台 / 文件 / 网络)、通知渠道(邮件 / 钉钉 / 短信)、支付网关(支付宝 / 微信 / PayPal)
- 判断标准很简单:
if config.PaymentProvider == "alipay"出现两次以上,就该用接口替代条件分支
接口方法签名要窄,但必须覆盖所有桥接点
常见错误是把接口设计成“大而全”,比如 type Payment interface { Pay(); Refund(); Query(); Cancel() },看似完整,实则埋雷。
- Pay 和 Refund 可能由不同系统承担(第三方网关只支持支付,内部系统才处理退款),硬塞进同一接口会导致部分实现返回
not implemented错误 - 更合理的切分是按能力契约:比如
type Payable interface { Pay(req PayRequest) (Resp, error) }、type Refundable interface { Refund(req RefundRequest) (Resp, error) } - 调用方按需组合:一个订单服务可能只依赖
Payable,对账服务才需要Refundable;这样新增 Apple Pay 支持时,只需实现Payable,不用动其他代码
组合字段必须显式初始化,否则 nil panic 是常态
桥接模式里最常踩的坑,就是抽象结构体持有一个接口字段,却忘了在构造函数里赋值。
- 写成
type OrderService struct { repo Repository }后直接 new,repo是 nil,调用s.repo.Save()立刻 panic - 正确做法是在构造函数中强制传入:
func NewOrderService(repo Repository) *OrderService { return &OrderService{repo: repo} } - 如果某实现暂时不需要依赖(比如本地内存版
InMemoryRepo),也要显式传入,而不是留空——空值不是“可选”,是“未定义” - Go 没有构造函数重载,所以别试图用可选参数绕过:
func NewOrderService(repo ...Repository)会诱导调用方传空 slice,反而更难排查
测试时别 mock 接口,要验证契约是否被真实满足
很多人写测试只关心 “mock 返回什么”,却忽略 “接口是否真被正确实现”。
- 用
var _ Payment = (*AlipayClient)(nil)这种空白赋值,在编译期就能捕获方法签名不匹配(比如少了个context.Context参数) - 单元测试里别只测 happy path,重点测边界:比如
Pay方法传入空PayRequest,实现是否返回明确错误(而非 panic 或静默失败) - 接口文档(注释)要写清前置条件和后置约束:比如
// Pay 要求 req.OrderID 非空,成功时保证幂等—— 这比代码更重要,因为它是调用方唯一能依赖的契约
接口不是设计出来的,是被变化逼出来的。你第一次写 if 分支时没抽接口,第二次加新渠道时犹豫了,第三次上线前夜重构才真正理解那行 repo.Save() 为什么非得是个接口。这种滞后感,恰恰是 Go 抽象建模最真实的样子。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











