在面向对象语言中,工厂模式几乎是写过几年代码的人都会碰到的设计模式。但在Go语言里,由于没有传统意义上的类和继承,一些开发者会误认为工厂模式不适用或实现起来很别扭。事实恰恰相反——Go的接口和多态机制让工厂模式变得非常自然,甚至可以比Java或C++更简洁。要踩的坑主要集中在包依赖管理和具体类型的暴露程度上,只要理顺了创建逻辑与调用方的关系,工厂模式就能成为Go工程中解耦对象的得力工具。

场景驱动——什么时候需要工厂
先看一个常见场景:你在写一个支付模块,可能需要对接支付宝、微信支付、银联支付等不同渠道。最简单的写法是在业务代码里用if或switch根据支付方式创建对应的处理器:
func GetPaymentHandler(method string) (PaymentHandler, error) {
switch method {
case "alipay":
return &AliPayHandler{}, nil
case "wechat":
return &WechatHandler{}, nil
case "unionpay":
return &UnionPayHandler{}, nil
default:
return nil, errors.New("unsupported payment method")
}
}
这段代码能工作,但问题也很明显:每新增一种支付渠道,你就要修改这个GetPaymentHandler函数,违反开闭原则。而且所有渠道的具体类型都硬编码在同一个包里,调用方需要显式引用AliPayHandler等结构体,一旦这些结构体内部实现发生变化,调用方也得跟着调整。工厂模式就是为了将对象的创建逻辑封装起来,让调用方只依赖抽象接口,而不需要知道具体类型。
在Go里,工厂模式的核心就是返回接口而非具体结构体。上面的例子实际上已经是一个简单工厂的雏形,但其扩展性还可以进一步优化。我们可以通过注册机制,让新增支付渠道时无需修改工厂函数本身,这就是下一个小节要展开的内容。
简单工厂与注册表——消灭switch分支
简单工厂是工厂模式中最基础的形式,它通过一个工厂函数根据参数返回不同的对象。当产品种类较少且变动不频繁时,这种做法足够简单高效。但正如前面提到的,一旦产品种类开始膨胀,switch分支就会失控。Go的init函数和全局注册表可以有效解决这个问题。
我们定义一个支付接口和一个工厂注册表。每个支付渠道在自己的包里实现接口,并通过init函数把自己注册到全局表里。这样工厂函数就只需要从表中查找,不再依赖具体的类型名称。代码如下:
// payment/payment.go
package payment
type PaymentHandler interface {
Pay(amount float64) error
}
var registry = make(map[string]func() PaymentHandler)
func Register(name string, factory func() PaymentHandler) {
registry[name] = factory
}
func GetPaymentHandler(name string) (PaymentHandler, error) {
factory, ok := registry[name]
if !ok {
return nil, fmt.Errorf("unknown payment method: %s", name)
}
return factory(), nil
}
// payment/alipay/alipay.go
package alipay
import "yourmodule/payment"
type AliPayHandler struct{}
func (h *AliPayHandler) Pay(amount float64) error {
// 调用支付宝接口
return nil
}
func init() {
payment.Register("alipay", func() payment.PaymentHandler {
return &AliPayHandler{}
})
}
这样,当需要新增微信支付时,只需要新建一个包,实现PaymentHandler并在init里注册即可,完全不需要改动payment包里的工厂代码。switch被彻底消除,模块间通过接口和注册表松耦合。唯一的注意事项是init函数的执行顺序:只要导入了具体的支付包(可以在main包中集中导入,或者使用空导入_ "yourmodule/payment/wechat"),注册就会自动完成。
工厂方法——让子类决定创建哪个产品
简单工厂把所有产品的创建逻辑集中在一个地方,当产品创建过程比较复杂或依赖外部资源时,这个工厂函数的职责就会过重。工厂方法模式将“创建哪一个对象”的决定推迟到子类(在Go中指实现了工厂接口的具体类型),每个具体工厂只负责创建一种产品。
在Go语言中,工厂方法通常表现为一个包含工厂函数的接口,或者是一个返回接口的构造函数。以数据库连接为例,假设我们需要支持MySQL和PostgreSQL,并且连接过程涉及驱动选择、参数配置等步骤。我们可以定义DBFactory接口,让不同的数据库包来实现该接口的CreateConnection方法:
type DBConnection interface {
Query(sql string) ([]Row, error)
}
type DBFactory interface {
CreateConnection(dsn string) (DBConnection, error)
}
MySQL的实现可能长这样:
type MySQLFactory struct{}
func (f *MySQLFactory) CreateConnection(dsn string) (DBConnection, error) {
db, err := sql.Open("mysql", dsn)
if err != nil {
return nil, err
}
return &MySQLConn{db: db}, nil
}
高层模块可以持有DBFactory接口,在运行时注入具体工厂,从而决定使用哪种数据库。这种模式在依赖注入框架中非常常见,也让单元测试变得容易——你可以轻松替换为一个返回Mock连接的工厂。
工厂方法模式的另一大优势是符合“单一职责原则”:每个具体工厂只负责一种产品的创建,产品本身的创建逻辑可以非常复杂,但不会污染其他工厂。在Go的项目中,如果发现一个New函数内部塞满了各种配置和分支,不妨考虑将其拆分为多个工厂方法,利用多态来分散复杂度。
抽象工厂——创建产品族
当系统需要创建一组相互关联或相互依赖的产品时,抽象工厂模式就派上了用场。比如一个UI框架需要支持Windows和macOS两套主题,每套主题包含按钮、文本框、窗口等控件,这些控件必须风格一致。抽象工厂定义了创建一系列产品的接口,具体工厂则负责同一产品族的所有对象的创建。
在Go中实现抽象工厂同样是基于接口。我们定义WidgetFactory接口,包含CreateButton() Button和CreateTextBox() TextBox,然后分别实现WindowsFactory和MacFactory,各自返回对应风格的控件。上层代码只依赖WidgetFactory接口,无需关心是哪个系统。
type Button interface {
Render()
}
type TextBox interface {
Render()
}
type WidgetFactory interface {
CreateButton() Button
CreateTextBox() TextBox
}
type WindowsFactory struct{}
func (f *WindowsFactory) CreateButton() Button {
return &WindowsButton{}
}
func (f *WindowsFactory) CreateTextBox() TextBox {
return &WindowsTextBox{}
}
type MacFactory struct{}
func (f *MacFactory) CreateButton() Button {
return &MacButton{}
}
func (f *MacFactory) CreateTextBox() TextBox {
return &MacTextBox{}
}
抽象工厂模式在Go中需要注意的问题是:产品的接口应该尽量稳定,因为新增一个产品(比如增加CreateMenu()方法)会导致所有具体工厂都要修改。如果产品族的变化是频繁的,可以考虑使用函数类型或者参数化的方式降低接口修改的影响。另外,由于Go没有泛型(Go 1.18之前),返回具体类型时需要类型断言,因此在设计接口时最好让返回的类型本身也是接口,保持灵活性。
在微服务架构中,抽象工厂也可用于创建不同环境下的代理客户端,例如测试环境和生产环境需要不同的RPC客户端栈,这时通过抽象工厂切换整个产品族就非常方便。
避免导入循环——工厂模式的包组织技巧
Go语言严格禁止包的循环导入,而工厂模式如果组织不当,很容易陷入循环依赖的困境。典型的问题场景是:工厂接口定义在factory包,产品接口定义在product包,具体产品在impl包,而具体工厂又在factory包或用到了product包,导致factory和product相互引用。
推荐的解耦方式是:将产品的接口定义在工厂包内(或者在单独的contract包),这样实现工厂的具体包只需要依赖接口包,而使用方也只依赖接口包,不会形成环。前文支付注册表的例子就是如此——PaymentHandler接口和Register函数同在payment包,具体支付包只导入payment并实现接口。调用方导入payment包和具体支付包(或只用_导入触发注册),但具体支付包不会反过来导入调用方。
另一种常见做法是使用internal或pkg目录分层,将工厂接口放在较高层(如domain层),具体实现放在子包中,通过依赖注入在编译时确定具体工厂的绑定关系,从而彻底消除硬编码的导入路径。
掌握了工厂模式在Go中的这三种形态以及包组织技巧,你就能在面对复杂对象创建逻辑时,写出更干净、更可测、更易扩展的代码。实际项目中不必拘泥于必须使用哪一种模式,而是根据变化频率和产品结构灵活组合,最终目标都是让代码对扩展开放,对修改关闭。
Golang工厂模式设计模式Go编程修改时间:2026-08-12 16:49:08