Go工厂模式实现简单工厂与抽象工厂
作者:白话机器学习
本篇讲解Go语言工厂模式,用map注册和switch实现简单工厂,定义工厂方法接口解耦创建逻辑,抽象工厂创建系列对象,结合依赖注入管理对象生命周期,分享map并发读写导致工厂panic的踩坑经验。
开篇故事
去年做支付网关,支持微信、支付宝、银联三种渠道。第一版用switch-case根据渠道名创建对应的支付客户端,代码写了三个分支。后来新增京东支付,改了工厂函数,测试时发现回归测试没覆盖到,上线后京东支付调用流程里有个参数没传对,线上失败了。
更麻烦的是,新增渠道要改工厂函数的代码,违反开闭原则。每次加渠道都要改switch,一不小心改错分支,影响已有渠道。后来重构成map注册的简单工厂,新渠道只需要实现接口并注册,工厂代码完全不改。
但map注册有个坑,启动时注册和运行时查找并发了,偶尔panic。这篇把几种工厂模式讲清楚,顺便说说怎么避开这个坑。
一、简单工厂:map注册加switch
简单工厂的核心思路是: 定义一个接口,不同实现注册到map里,运行时按类型名查找对应的构造函数。
package payment
import "errors"
// PaymentChannel 支付渠道接口
// 所有支付渠道实现这个接口
type PaymentChannel interface {
// Pay 发起支付
Pay(amount int, orderID string) (string, error)
// Query 查询支付状态
Query(orderID string) (string, error)
// Name 渠道名称
Name() string
}
// Constructor 构造函数类型
// 每个渠道注册一个构造函数
type Constructor func() PaymentChannel
// factory 简单工厂
type factory struct {
// 用map保存类型名到构造函数的映射
creators map[string]Constructor
}
// NewFactory 创建工厂实例
func NewFactory() *factory {
return &factory{
creators: make(map[string]Constructor),
}
}
// Register 注册渠道构造函数
// type是渠道标识,如"wechat"、"alipay"
func (f *factory) Register(typ string, c Constructor) {
f.creators[typ] = c
}
// Create 根据类型创建支付渠道实例
func (f *factory) Create(typ string) (PaymentChannel, error) {
constructor, ok := f.creators[typ]
if !ok {
return nil, errors.New("unsupported payment type: " + typ)
}
return constructor(), nil
}
// --- 微信支付实现 ---
// WeChatPay 微信支付渠道,导出类型供外部包使用
type WeChatPay struct{}
func (w *WeChatPay) Pay(amount int, orderID string) (string, error) {
return "wechat_pay_" + orderID, nil
}
func (w *WeChatPay) Query(orderID string) (string, error) {
return "paid", nil
}
func (w *WeChatPay) Name() string { return "wechat" }
// --- 支付宝支付实现 ---
// Alipay 支付宝支付渠道,导出类型供外部包使用
type Alipay struct{}
func (a *Alipay) Pay(amount int, orderID string) (string, error) {
return "alipay_" + orderID, nil
}
func (a *Alipay) Query(orderID string) (string, error) {
return "paid", nil
}
func (a *Alipay) Name() string { return "alipay" }
使用时先注册所有渠道,再按需创建。
package main
import "payment"
func main() {
f := payment.NewFactory()
// 启动时注册所有渠道
f.Register("wechat", func() payment.PaymentChannel { return &payment.WeChatPay{} })
f.Register("alipay", func() payment.PaymentChannel { return &payment.Alipay{} })
// 运行时按类型创建
channel, err := f.Create("wechat")
if err != nil {
panic(err)
}
channel.Pay(100, "order_123")
}
map注册的好处是新增渠道不用改工厂代码。实现接口,写个构造函数,调用Register注册,工厂的Create逻辑完全不变。
二、工厂方法接口
简单工厂把所有创建逻辑集中在一个地方。工厂方法模式把创建逻辑分散到子工厂,每个产品类型有自己的工厂。
package storage
import "errors"
// Storage 存储接口
type Storage interface {
Save(key string, data []byte) error
Load(key string) ([]byte, error)
Delete(key string) error
}
// StorageFactory 工厂方法接口
// 每种存储类型实现自己的工厂
type StorageFactory interface {
// Create 创建存储实例
Create(config map[string]string) (Storage, error)
// Type 工厂支持的存储类型
Type() string
}
// --- 本地文件存储 ---
type fileStorage struct {
dir string
}
func (f *fileStorage) Save(key string, data []byte) error {
return nil
}
func (f *fileStorage) Load(key string) ([]byte, error) {
return nil, nil
}
func (f *fileStorage) Delete(key string) error {
return nil
}
type fileStorageFactory struct{}
func (f *fileStorageFactory) Create(config map[string]string) (Storage, error) {
dir, ok := config["dir"]
if !ok {
return nil, errors.New("file storage需要dir配置")
}
return &fileStorage{dir: dir}, nil
}
func (f *fileStorageFactory) Type() string { return "file" }
// --- S3存储 ---
type s3Storage struct {
bucket string
region string
}
func (s *s3Storage) Save(key string, data []byte) error {
return nil
}
func (s *s3Storage) Load(key string) ([]byte, error) {
return nil, nil
}
func (s *s3Storage) Delete(key string) error {
return nil
}
type s3StorageFactory struct{}
func (f *s3StorageFactory) Create(config map[string]string) (Storage, error) {
bucket := config["bucket"]
region := config["region"]
if bucket == "" {
return nil, errors.New("s3 storage需要bucket配置")
}
return &s3Storage{bucket: bucket, region: region}, nil
}
func (f *s3StorageFactory) Type() string { return "s3" }
// --- 工厂注册中心 ---
type factoryRegistry struct {
factories map[string]StorageFactory
}
func NewFactoryRegistry() *factoryRegistry {
return &factoryRegistry{factories: make(map[string]StorageFactory)}
}
func (r *factoryRegistry) Register(f StorageFactory) {
r.factories[f.Type()] = f
}
func (r *factoryRegistry) Create(typ string, config map[string]string) (Storage, error) {
f, ok := r.factories[typ]
if !ok {
return nil, errors.New("unsupported storage type: " + typ)
}
return f.Create(config)
}
工厂方法和简单工厂的区别在于: 简单工厂的工厂类知道所有产品的创建逻辑,工厂方法把创建逻辑下放到具体工厂类。新增产品时,工厂方法模式只需要加一个工厂类并注册,注册中心的代码不变。Go没有继承,工厂方法的"子工厂"用接口实现代替。
三、踩坑经验:map并发读写导致工厂panic
这个坑我踩过。支付工厂在启动时注册渠道,运行时查找渠道。某次加了热加载功能,运行时动态注册新渠道,同时业务请求在查找渠道。偶尔panic: concurrent map read and map write。
Go的map不是并发安全的。同时读写map会触发runtime fatal error,整个goroutine直接panic,recover都接不住。
package payment
import (
"errors"
"sync"
)
// SafeFactory 并发安全的工厂
// 用sync.RWMutex保护map
type SafeFactory struct {
mu sync.RWMutex
creators map[string]Constructor
}
func NewSafeFactory() *SafeFactory {
return &SafeFactory{
creators: make(map[string]Constructor),
}
}
// Register 注册渠道,加写锁
func (f *SafeFactory) Register(typ string, c Constructor) {
f.mu.Lock()
defer f.mu.Unlock()
f.creators[typ] = c
}
// Create 创建渠道,加读锁
func (f *SafeFactory) Create(typ string) (PaymentChannel, error) {
// 读锁: 允许多个goroutine同时读
f.mu.RLock()
constructor, ok := f.creators[typ]
f.mu.RUnlock()
if !ok {
return nil, errors.New("unsupported type: " + typ)
}
return constructor(), nil
}
// Unregister 注销渠道(热卸载场景)
func (f *SafeFactory) Unregister(typ string) {
f.mu.Lock()
defer f.mu.Unlock()
delete(f.creators, typ)
}
// List 列出所有已注册的类型
func (f *SafeFactory) List() []string {
f.mu.RLock()
defer f.mu.RUnlock()
types := make([]string, 0, len(f.creators))
for typ := range f.creators {
types = append(types, typ)
}
return types
}
// PaymentChannel 支付渠道接口
type PaymentChannel interface {
Pay(amount int, orderID string) (string, error)
Query(orderID string) (string, error)
Name() string
}
// Constructor 构造函数类型
type Constructor func() PaymentChannel
关键点是用sync.RWMutex替代普通map。读多写少场景用RWMutex,读操作加读锁不阻塞其他读操作,写操作加写锁独占。如果工厂在启动后不再注册新渠道,可以用sync.Map,读操作完全无锁,性能更好。
四、抽象工厂创建系列对象
抽象工厂创建一系列相关对象。比如一个UI框架,Windows风格创建WindowsButton和WindowsDialog,Mac风格创建MacButton和MacDialog。抽象工厂保证这组对象风格一致。
package ui
// Button 按钮接口
type Button interface {
Render() string
OnClick(callback func())
}
// Dialog 对话框接口
type Dialog interface {
Show(title string)
Close()
}
// UIFactory 抽象工厂接口
// 创建一组相关的UI组件,保证风格一致
type UIFactory interface {
CreateButton() Button
CreateDialog() Dialog
Theme() string
}
// --- Windows风格实现 ---
type winButton struct{}
func (b *winButton) Render() string { return "[Win Button]" }
func (b *winButton) OnClick(f func()) { f() }
type winDialog struct{}
func (d *winDialog) Show(title string) {}
func (d *winDialog) Close() {}
type winFactory struct{}
func (f *winFactory) CreateButton() Button { return &winButton{} }
func (f *winFactory) CreateDialog() Dialog { return &winDialog{} }
func (f *winFactory) Theme() string { return "windows" }
// --- Mac风格实现 ---
type macButton struct{}
func (b *macButton) Render() string { return "[Mac Button]" }
func (b *macButton) OnClick(f func()) { f() }
type macDialog struct{}
func (d *macDialog) Show(title string) {}
func (d *macDialog) Close() {}
type macFactory struct{}
func (f *macFactory) CreateButton() Button { return &macButton{} }
func (f *macFactory) CreateDialog() Dialog { return &macDialog{} }
func (f *macFactory) Theme() string { return "mac" }
// NewUIFactory 根据主题创建抽象工厂
// 工厂的工厂,按需选择具体工厂
func NewUIFactory(theme string) UIFactory {
switch theme {
case "windows":
return &winFactory{}
case "mac":
return &macFactory{}
default:
return &winFactory{}
}
}
抽象工厂和DI容器结合时,工厂从容器拿依赖。比如S3StorageFactory创建S3Storage时需要AWS client,从DI容器注入。工厂负责创建对象,DI容器负责管理依赖,工厂从容器拿依赖注入到产品中,解耦了对象间的依赖关系。
五、对比分析
| 工厂方案 | 扩展性 | 创建逻辑分布 | 并发安全 | 适用场景 |
|---|---|---|---|---|
| 简单工厂 | 好(map注册) | 集中 | 需加锁 | 产品类型固定 |
| 工厂方法 | 好 | 分散到子工厂 | 需加锁 | 创建逻辑复杂 |
| 抽象工厂 | 差(加系列改接口) | 集中 | 需加锁 | 创建对象系列 |
| switch工厂 | 差 | 集中 | 无需 | 产品极少 |
简单工厂用map注册,新增产品不改工厂代码,扩展性最好。工厂方法把创建逻辑分散到各子工厂,每个子工厂独立变化。抽象工厂保证一组产品风格一致,但加新产品系列要改接口。最原始的switch-case工厂适合产品极少且不会增长的场景。
总结
工厂模式用接口隔离创建逻辑和业务代码。简单工厂用map注册构造函数,新增产品不改工厂代码,扩展性好。Go的map非并发安全,运行时增删产品要加锁,用sync.RWMutex或sync.Map。抽象工厂创建一系列相关对象,保证风格一致。工厂和DI结合时,工厂从容器拿依赖注入到产品中。下一篇我们聊观察者模式,看看怎么用channel实现事件总线。
到此这篇关于Go工厂模式实现简单工厂与抽象工厂的文章就介绍到这了,更多相关Go工厂模式内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
