在Go语言里,创建对象并不只有结构体字面量这一种方式。当一个结构体的字段需要统一校验、默认值需要集中维护,或者调用方不应依赖具体实现时,工厂模式提供了一层清晰的间接边界。Go虽然没有构造函数语法,但可以通过包内工厂函数来承担构造函数职责;如果希望进一步解耦,可以返回接口类型,把具体实现完全隐藏。本文从订单通知和支付渠道两个常见场景切入,依次介绍工厂函数、简单工厂、工厂方法以及抽象工厂的实现方式,并讨论它们各自的适用条件。

为什么Go语言需要工厂模式
Go语言的结构体没有构造函数语法,结构体字段一旦设置为未导出,包外部就无法直接通过字面量赋值。比如下面这个场景,如果User结构体的字段首字母小写,外部包试图用User{name: "tom"}创建对象会直接编译失败。此时工厂函数就成了唯一合法的创建入口,它可以在返回对象前完成参数校验、默认值填充以及内部状态初始化。
另一个更实际的原因是对象创建往往伴随着业务规则。例如订单通知服务要求接收人邮箱格式正确,短信通知服务要求手机号必须是合法格式。如果在每个调用点都分散编写这些校验逻辑,代码会很快变得不可维护。工厂函数把这些规则集中到一处,调用方只需要传入原始参数,不必重复判断合法性。Go标准库中也大量使用这种模式,比如net/http包中的NewRequest函数,它在内部处理了方法名、URL和请求体等参数,并返回一个已初始化的Request指针。
package user
import "errors"
type User struct {
name string
email string
}
func NewUser(name, email string) (*User, error) {
if name == "" {
return nil, errors.New("name cannot be empty")
}
if email == "" {
return nil, errors.New("email cannot be empty")
}
return &User{
name: name,
email: email,
}, nil
}
这个工厂函数返回具体类型*User,它适合调用方确实需要操作User自身字段或方法的场景。返回具体指针的好处是简单直观,调用方可以继续使用该类型的所有方法,同时隐藏了字段的初始化细节。不过它仍然暴露了具体类型,当业务需要替换实现时会受到限制。
用简单工厂返回接口隐藏实现
简单工厂的核心是定义一个接口,然后让多个具体类型实现该接口,再由工厂函数根据标识返回对应的实现。这样做最大的好处是调用方只依赖接口,不关心背后是支付宝还是微信支付,后续增加新的支付渠道时,调用方代码无需改动。
下面是一个支付渠道的示例。首先定义Payment接口,包含Pay方法。然后分别定义Alipay和WechatPay两个结构体,并让它们实现该接口。工厂函数NewPayment接收一个字符串参数,通过switch分支返回对应的实现。如果传入未知渠道,则返回错误,避免程序继续执行时出现空指针。
package payment
import "fmt"
type Payment interface {
Pay(amount float64) error
}
type Alipay struct{}
func (a Alipay) Pay(amount float64) error {
fmt.Printf("alipay pay %.2f\n", amount)
return nil
}
type WechatPay struct{}
func (w WechatPay) Pay(amount float64) error {
fmt.Printf("wechat pay %.2f\n", amount)
return nil
}
func NewPayment(method string) (Payment, error) {
switch method {
case "alipay":
return Alipay{}, nil
case "wechat":
return WechatPay{}, nil
default:
return nil, fmt.Errorf("unsupported payment method: %s", method)
}
}
这种写法在Go项目中非常常见。调用方只需要获取到一个Payment接口值,就可以安全地调用Pay方法。具体的支付渠道细节被完全隐藏在工厂函数内部。当需要新增银联支付时,只需添加一个UnionPay结构体实现接口,并在switch中增加一个分支,所有调用方无需感知变化。
但简单工厂也有局限。如果创建逻辑本身开始膨胀,例如不同渠道需要注入不同的配置对象、密钥管理器或日志组件,把所有逻辑都堆在一个switch里并不利于维护。这时可以考虑把工厂函数升级为工厂方法,让每个具体产品对应一个具体工厂。
工厂方法模式让每个实现拥有独立工厂
工厂方法模式将对象创建逻辑下沉到一个个独立的工厂结构体中。Go语言没有继承,因此通常会定义两个接口:一个表示产品,一个表示工厂。具体工厂负责创建具体产品,调用方只依赖工厂接口。这样做的好处是新增产品时,不需要修改已有的工厂函数,只需添加一个新的工厂类型,符合开闭原则。
以通知服务为例,假设系统需要发送短信和邮件。首先定义Notifier产品接口,以及NotifierFactory工厂接口。然后分别实现SMSNotifier、EmailNotifier,以及对应的SMSFactory、EmailFactory。每个具体工厂的Create方法只负责返回自己的产品。
package notify
type Notifier interface {
Send(to, message string) error
}
type SMSNotifier struct{}
func (s SMSNotifier) Send(to, message string) error {
return nil
}
type EmailNotifier struct{}
func (e EmailNotifier) Send(to, message string) error {
return nil
}
type NotifierFactory interface {
Create() Notifier
}
type SMSFactory struct{}
func (f SMSFactory) Create() Notifier {
return SMSNotifier{}
}
type EmailFactory struct{}
func (f EmailFactory) Create() Notifier {
return EmailNotifier{}
}
实际使用时,调用方可以先根据配置选择一个具体工厂,再通过工厂接口创建产品。比如配置文件中指定通知类型为sms,程序就使用SMSFactory;如果改成email,只需替换工厂实例,产品创建逻辑和后续调用逻辑完全不变。这种方式让创建逻辑的扩展点更加清晰,每个工厂的职责单一,适合创建逻辑复杂且产品类型较多的场景。
工厂方法模式的成本在于类型数量会增加,需要为每个产品额外定义一个工厂结构体。当产品只有两三种、创建逻辑也不复杂时,使用简单工厂反而更直接。选择哪种方式,通常取决于项目的长期维护预期和团队对扩展性的要求。
抽象工厂组合相关对象的创建
抽象工厂解决的问题更进一步:如果一个产品族包含多个相互关联的对象,比如发送通知时需要同时创建发送器和模板渲染器,那么单独使用工厂方法会产生多个工厂接口,调用方需要分别管理这些工厂。抽象工厂将它们组合到一个接口中,让一次调用可以获取一族相关对象。
假设通知系统包含两个产品:Sender负责发送消息,Template负责渲染消息内容。不同渠道可能使用不同的发送器和模板。抽象工厂接口NotificationFactory声明两个创建方法,具体工厂SMSNotificationFactory同时返回短信发送器和短信模板,确保二者来自同一个产品族,不会出现邮件模板搭配短信发送器的错误组合。
package message
type Template interface {
Render(data map[string]string) string
}
type Sender interface {
Send(to, content string) error
}
type NotificationFactory interface {
CreateSender() Sender
CreateTemplate() Template
}
type SMSSender struct{}
func (s SMSSender) Send(to, content string) error {
return nil
}
type SMSTemplate struct{}
func (t SMSTemplate) Render(data map[string]string) string {
return "sms content"
}
type SMSNotificationFactory struct{}
func (f SMSNotificationFactory) CreateSender() Sender {
return SMSSender{}
}
func (f SMSNotificationFactory) CreateTemplate() Template {
return SMSTemplate{}
}
抽象工厂在Go语言中并不像在Java中那样频繁出现,因为Go的接口设计更倾向于小而精。但如果项目中确实存在一组对象需要保持兼容性,抽象工厂能有效减少误用。它的另一个典型应用是数据库访问层,不同数据库需要同时创建连接器、事务管理器和SQL方言处理器,抽象工厂可以保证这些组件来自同一数据库实现。
需要注意的是,抽象工厂增加了接口的复杂性,每个新增产品都会要求修改工厂接口以及所有具体工厂,这对已有代码会造成一定侵入。因此只有在产品族相对稳定、且确实需要保证族内对象一致性时才适合使用。
工厂模式实践中的几个关键选择
返回具体类型还是接口,是使用工厂模式时最先要做的决定。如果返回具体类型,调用方可以访问该类型的全部方法,但代价是耦合到具体实现。如果返回接口,调用方只依赖行为契约,扩展性更好,但可能丢失某些特有方法。一般原则是:当业务可能替换实现时返回接口;当结构体本身就是稳定的值对象时返回具体类型。
错误处理也必须放在工厂函数内部完成。Go语言没有异常机制,工厂函数通常返回(T, error),调用方必须检查错误。不要因为追求简洁而返回零值或nil,否则后续调用很容易触发空指针异常。对于参数校验失败、配置缺失或初始化依赖失败等情况,都应当返回明确的错误信息,便于定位问题。
工厂模式不应被滥用。如果一个结构体创建非常简单,直接使用字面量完全够用,就没有必要包一层工厂函数。过度设计会让代码跳转层级增加,阅读成本上升。判断标准很简单:如果创建对象时存在校验、默认值、依赖注入或类型选择中的任意一项,工厂模式才是合理的;如果这些都没有,保持直接实例化反而更清晰。测试时,工厂返回接口的优势尤其明显,可以轻松替换成模拟实现,而无需修改业务逻辑。

