工厂函数必须返回接口类型而非具体结构体指针,以解耦调用方与实现细节;接口应小而专注,方法按需定义;实现需用指针接收者以支持状态修改;参数宜用具名常量而非魔法字符串。

工厂函数必须返回接口类型,而不是具体结构体指针
Go 语言里没有“抽象基类”或“构造函数重载”,所谓工厂模式的本质,就是让调用方只依赖行为契约(接口),不感知实现细节。如果工厂函数签名是 func() *JsonDecoder 或 func() Alipay,那调用方立刻和具体类型耦合了——换一种支付方式就得改所有调用点。
正确做法是定义统一接口,工厂返回该接口:
type Payment interface {
Pay(amount float64) error
}
func NewPayment(kind string) Payment {
switch kind {
case "alipay":
return &Alipay{}
case "wechat":
return &WechatPay{}
default:
return &MockPay{}
}
}
- 接口类型本身是编译期静态检查的,类型断言失败会在编译时报错,不是运行时 panic
- 返回值类型是
Payment,不是*Alipay,调用方无法访问Alipay的私有字段或未导出方法 - 测试时可直接传入 mock 实现,无需修改工厂逻辑
具体类型要用指针接收者实现接口方法
如果你写的是 func (a Alipay) Pay(...)(值接收者),那么 &Alipay{} 和 Alipay{} 都满足 Payment 接口;但一旦结构体里有需要修改状态的字段(比如计数器、连接池、缓存 map),值接收者就会操作副本,原结构体不变——这通常不是你想要的。
更关键的是:若接口方法需要修改 receiver,而你用了值接收者,Go 编译器会静默拒绝该类型实现接口(哪怕方法名、签名都对)。
立即学习“go语言免费学习笔记(深入)”;
- 只要结构体含可变状态,或方法签名中涉及指针语义(如
Write([]byte)内部要改缓冲区),就用func (p *T) Method() - 小结构体(如
type ID int)且方法纯读取,值接收者也可,但统一用指针接收者更安全、更一致 - 工厂内部 new 出结构体后,必须返回其指针才能满足指针接收者接口要求:
return &Alipay{},不能是return Alipay{}
工厂函数参数不宜过度泛化,避免类型字符串硬编码
用字符串(如 "alipay")控制创建逻辑,看似灵活,实则脆弱:拼错、大小写不一致、无编译检查。线上出问题往往就卡在这儿。
更可控的方式是用具名常量或配置结构体:
type PaymentType int
const (
AlipayType PaymentType = iota
WechatType
MockType
)
func NewPayment(t PaymentType) Payment {
switch t {
case AlipayType:
return &Alipay{}
case WechatType:
return &WechatPay{}
default:
return &MockPay{}
}
}
- 类型错误在编译期暴露,IDE 能自动补全,重构安全
- 若需从配置文件加载,可在上层做一次字符串到
PaymentType的映射,把解析逻辑收口,不污染工厂函数 - 完全避免在业务代码里出现
NewPayment("alipay")这种魔法字符串
接口定义要小而专注,别试图塞进所有可能方法
一个叫 Logger 的接口,如果一开始就定义了 Info()、Error()、Debug()、WithField()、Sync()……那每新增一种日志实现,都要补一堆空实现或 panic,违背“小接口”原则。
真正松耦合的接口,只约定当前调用方**确实需要**的行为:
- 如果只是统一打错误日志,接口只需
LogError(msg string) - 如果调用方要自己格式化再输出,接口可以是
Write([]byte) (int, error)—— 和标准库io.Writer对齐,复用生态 - 后续扩展新能力(如上下文支持),应新增接口(
ContextLogger),而非往老接口加方法,否则破坏已有实现
接口膨胀是最隐蔽的耦合源——它让你以为解耦了,实际所有实现都被绑死在同一个大契约上。


















