工厂模式的核心目标是把对象的创建逻辑从使用逻辑中分离出来,让调用方不需要关心具体类型是如何构造的。Go语言没有传统意义上的类和构造函数重载,但它提供了接口、结构体以及一等公民函数这些特性,恰恰为工厂模式提供了非常灵活的实现土壤。本文将从最基础的简单工厂讲起,逐步演进到工厂方法、注册式工厂,并结合日志系统这个贯穿全文的例子,给出可以直接运行的代码。

为什么Go语言需要工厂模式
在Java或C++这类语言中,工厂模式往往与继承体系绑定在一起,通过子类重写工厂方法来决定创建哪种产品。Go没有继承,只有组合与接口,因此很多人初学Go时会疑惑:既然可以用NewXxx函数直接创建对象,还有必要搞工厂吗?答案是肯定的,而且Go的接口让工厂模式变得更轻量。
考虑一个典型场景:你的项目需要支持多种日志后端,比如输出到控制台、文件或者远程服务。如果业务代码里到处写switch logType { case "console": ... },一旦新增一种后端,所有调用点都要修改,这明显违反了开闭原则。工厂模式的作用就是把这种变化收敛到一处,调用方只依赖统一的接口,具体类型的选择完全交给工厂负责。
Go语言中的接口是隐式实现的,结构体只要实现了接口定义的方法集就自动满足该接口,这使得新增产品类型时完全不需要修改已有的抽象层代码。这种特性与工厂模式结合后,扩展成本极低,也是Go标准库中大量使用工厂思想的根本原因,例如database/sql包通过sql.Open配合驱动注册机制,实现了对不同数据库的统一访问。
简单工厂:最直接的实现方式
简单工厂并不属于GoF二十三种设计模式,但它是理解工厂模式的最佳起点。实现思路非常朴素:定义一个产品接口,再定义一个接受参数的工厂函数,根据参数返回不同的具体实现。
package main
import "fmt"
// Logger 是产品接口,所有日志后端都实现它
type Logger interface {
Log(msg string)
}
// ConsoleLogger 控制台实现
type ConsoleLogger struct{}
func (c *ConsoleLogger) Log(msg string) {
fmt.Println("[CONSOLE]", msg)
}
// FileLogger 文件实现
type FileLogger struct {
Path string
}
func (f *FileLogger) Log(msg string) {
fmt.Println("[FILE:"+f.Path+"]", msg)
}
// NewLogger 简单工厂函数,根据类型返回不同实现
func NewLogger(logType string) (Logger, error) {
switch logType {
case "console":
return &ConsoleLogger{}, nil
case "file":
return &FileLogger{Path: "app.log"}, nil
default:
return nil, fmt.Errorf("不支持的日志类型: %s", logType)
}
}
func main() {
logger, err := NewLogger("console")
if err != nil {
panic(err)
}
logger.Log("hello factory pattern")
}这段代码的要点在于NewLogger返回的是Logger接口而不是具体结构体,调用方拿到的对象只能调用Log方法,对内部实现一无所知。简单工厂的优点是实现简单、容易理解,适合产品种类较少且相对稳定的场景。
它的缺点也很明显:每新增一种日志后端,都必须修改NewLogger函数中的switch分支,工厂函数会随着产品数量膨胀变得臃肿。如果产品种类预计会持续增长,就需要更灵活的方案。
工厂方法模式:用函数类型实现延迟创建
经典的工厂方法模式通过让子类决定实例化哪个对象来解耦,Go中可以用函数类型和结构体组合来模拟这种结构。核心思路是:把工厂本身也抽象成一个接口,每个具体工厂只负责创建一种产品。
package main
import "fmt"
// Logger 产品接口
type Logger interface {
Log(msg string)
}
// LoggerFactory 工厂接口,Create方法返回产品
type LoggerFactory interface {
Create() Logger
}
// ConsoleFactory 负责创建控制台日志
type ConsoleFactory struct{}
func (c *ConsoleFactory) Create() Logger {
return &ConsoleLogger{}
}
// ConsoleLogger 具体产品
type ConsoleLogger struct{}
func (c *ConsoleLogger) Log(msg string) {
fmt.Println("[CONSOLE]", msg)
}
// UseLogger 调用方只依赖工厂接口和产品接口
func UseLogger(f LoggerFactory) {
logger := f.Create()
logger.Log("created by factory method")
}
func main() {
UseLogger(&ConsoleFactory{})
}这种结构下,新增一种日志后端只需要新增一个产品结构体和对应的工厂结构体,完全不需要改动任何已有代码,严格遵循开闭原则。调用方通过依赖注入的方式接收LoggerFactory,在单元测试中可以很方便地传入Mock工厂,这也是工厂方法在测试驱动开发中广受欢迎的原因。
不过Go社区还有一种更地道的写法:由于函数在Go中是一等公民,工厂接口可以直接简化为函数类型,写成type LoggerCreator func() Logger,任何匹配签名的函数都能作为工厂传入,代码量进一步减少。这种函数式风格的工厂方法在Go标准库和开源项目中出现频率极高,值得优先掌握。
注册式工厂:借鉴database/sql的驱动机制
当产品种类非常多,或者产品实现分散在不同包甚至由第三方提供时,可以为每种实现编写注册代码,工厂内部只维护一张注册表。Go标准库的database/sql正是这种设计:sql.Register负责登记驱动,sql.Open根据名字查找并创建连接,数据库驱动包通常在init函数中完成自注册。
package main
import (
"fmt"
"sync"
)
// Logger 产品接口
type Logger interface {
Log(msg string)
}
// registry 全局注册表,带读写锁保证并发安全
var (
registry = make(map[string]func() Logger)
regMu sync.RWMutex
)
// RegisterLogger 注册一种日志实现
func RegisterLogger(name string, creator func() Logger) {
regMu.Lock()
defer regMu.Unlock()
registry[name] = creator
}
// GetLogger 按名称获取日志实例
func GetLogger(name string) (Logger, error) {
regMu.RLock()
defer regMu.RUnlock()
creator, ok := registry[name]
if !ok {
return nil, fmt.Errorf("未注册的日志类型: %s", name)
}
return creator(), nil
}
// FileLogger 具体产品
type FileLogger struct{}
func (f *FileLogger) Log(msg string) {
fmt.Println("[FILE]", msg)
}
func init() {
// 驱动自注册,类似数据库驱动的init函数
RegisterLogger("file", func() Logger {
return &FileLogger{}
})
}
func main() {
logger, err := GetLogger("file")
if err != nil {
panic(err)
}
logger.Log("registry factory works")
}注册式工厂的最大价值在于解耦了产品定义与工厂本体。第三方库只需导入自己的包并在init中注册,主程序通过字符串名字就能拿到实现,工厂代码本身零修改。配合sync.RWMutex还能保证并发场景下的注册与查找安全。
这种方案的代价是引入了全局状态,注册发生在init阶段时错误不易被及时发现,排查未注册的问题也需要额外日志。因此在中小型项目中,建议优先使用简单工厂或函数式工厂方法,只有当产品由外部插件式提供时再引入注册表机制。
方案对比与选型建议
三种实现没有绝对优劣,关键看产品数量的变化频率和来源。下表 summarizes了各自的适用场景:
| 实现方式 | 扩展方式 | 适用场景 |
|---|---|---|
| 简单工厂 | 修改switch分支 | 产品种类少且稳定 |
| 工厂方法 | 新增工厂与产品结构体 | 需要依赖注入和测试替身 |
| 注册式工厂 | init函数自注册 | 插件化、第三方扩展 |
实际编码中还有几点经验值得注意。第一,工厂函数返回值应尽量是接口类型而不是具体结构体指针,这样调用方才能真正与实现解耦;第二,如果创建过程可能失败,返回(Logger, error)两个值是Go的惯例,不要在工厂内部悄悄返回半成品对象;第三,当构造参数较多时,可以考虑传入配置结构体或者使用Option模式,避免工厂函数签名无限膨胀。
总结来说,Go语言实现工厂模式没有固定套路,接口加函数类型的组合足以覆盖绝大多数需求。从简单工厂起步,在扩展压力出现时演进到工厂方法或注册式工厂,保持调用方只依赖抽象这一原则不变,你的代码就能在需求变化面前保持足够的弹性。
Golang工厂模式工厂方法设计模式修改时间:2026-09-01 14:26:42