Go语言设计模式入门:简单工厂和单例怎么用?
作者:FfHUCisI
一、设计模式是干什么的
写代码久了你会发现,很多问题在反复出现:怎么保证全局只有一个数据库连接?怎么根据配置创建不同类型的对象?怎么一步步构建一个参数多到令人发指的结构体?
前辈们把这些问题的最佳解法总结成了设计模式——不是死板的代码模板,而是一种"遇到这种情况就这样处理"的经验结晶。
Go 语言和 Java/C++ 的设计模式在实现上差异很大。Go 没有类继承,没有构造函数重载,但它有接口、组合、包级别的封装,这让很多模式在 Go 里的写法更加轻量和自然。
创建型模式解决的是**“对象怎么创建”**的问题。今天先学最常用的四种:简单工厂、工厂方法、抽象工厂和单例。
二、简单工厂模式(Simple Factory)
问题场景
假设你在做一个支付系统,用户可以选择微信支付或支付宝支付。
两种支付方式的接口一样(都是 Pay(amount)),但内部逻辑完全不同。
你不想在每个需要支付的地方都写 if type == "wechat" { ... } else { ... },那样代码会充满重复的判断逻辑。
解决思路
把"根据参数决定创建哪个对象"的逻辑集中到一个工厂函数里。
调用方只需要告诉工厂"我要什么",不需要知道具体怎么创建。
// 支付接口
type Payment interface {
Pay(amount float64) string
}
// 微信支付
type WechatPay struct{}
func (w *WechatPay) Pay(amount float64) string {
return fmt.Sprintf("微信支付:¥%.2f", amount)
}
// 支付宝支付
type Alipay struct{}
func (a *Alipay) Pay(amount float64) string {
return fmt.Sprintf("支付宝支付:¥%.2f", amount)
}
// 简单工厂:根据类型返回对应的支付实例
func NewPayment(payType string) Payment {
switch payType {
case "wechat":
return &WechatPay{}
case "alipay":
return &Alipay{}
default:
return nil
}
}
优缺点
优点:把创建逻辑集中管理,调用方和具体实现解耦。新增支付方式只需在工厂里加一个 case。
缺点:工厂函数里的 switch 会越来越长,违反开闭原则(对扩展开放、对修改关闭)。每加一种新产品就要改工厂代码。
三、工厂方法模式(Factory Method)
问题场景
简单工厂的问题是:工厂本身知道所有产品的创建细节。
当产品种类变多、创建逻辑变复杂时,工厂就变成了一个巨大的 switch。而且,如果你想让不同的"车间"生产不同的产品呢?
解决思路
把"创建产品"这件事也抽象成接口。每种产品对应一个工厂,工厂方法模式的核心是**“让子类决定实例化哪个类”**。
// 产品接口
type Logger interface {
Log(message string)
}
// 具体产品:文件日志
type FileLogger struct {
filePath string
}
func (f *FileLogger) Log(message string) {
fmt.Printf("[文件日志 %s] %s\n", f.filePath, message)
}
// 具体产品:控制台日志
type ConsoleLogger struct{}
func (c *ConsoleLogger) Log(message string) {
fmt.Printf("[控制台] %s\n", message)
}
// 工厂接口:每个具体工厂返回一个 Logger
type LoggerFactory interface {
CreateLogger() Logger
}
// 文件日志工厂
type FileLoggerFactory struct {
Path string
}
func (f *FileLoggerFactory) CreateLogger() Logger {
return &FileLogger{filePath: f.Path}
}
// 控制台日志工厂
type ConsoleLoggerFactory struct{}
func (c *ConsoleLoggerFactory) CreateLogger() Logger {
return &ConsoleLogger{}
}
使用方式
func UseLogger(factory LoggerFactory) {
logger := factory.CreateLogger()
logger.Log("系统启动")
}
// 切换日志方式只需换工厂
UseLogger(&ConsoleLoggerFactory{})
UseLogger(&FileLoggerFactory{Path: "/var/log/app.log"})
优缺点
优点:符合开闭原则。新增产品 = 新增工厂类,不需要改已有代码。
缺点:类的数量翻倍。每新增一个产品就要新增一个工厂,项目小的时候显得过度设计。
四、抽象工厂模式(Abstract Factory)
问题场景
工厂方法只能创建一种产品。但现实中经常需要创建一组相关的产品。
举个例子:一个 UI 框架要同时支持 Windows 和 Mac 两套主题。每套主题都包含 Button 和 Checkbox 两个组件。
你不可能让用户自己分别创建 WindowsButton + WindowsCheckbox 或者 MacButton + MacCheckbox——那样很容易把不同主题的组件混在一起。
解决思路
抽象工厂定义了一组创建方法,每个方法创建一种产品。不同的具体工厂实现整组产品的创建。
// 产品接口
type Button interface {
Render() string
}
type Checkbox interface {
Toggle() string
}
// 抽象工厂:能创建一组产品
type UIFactory interface {
CreateButton() Button
CreateCheckbox() Checkbox
}
// ===== Windows 主题 =====
type WindowsButton struct{}
func (w *WindowsButton) Render() string { return "[Windows 风格按钮]" }
type WindowsCheckbox struct{}
func (w *WindowsCheckbox) Toggle() string { return "[Windows 复选框: ✓]" }
type WindowsFactory struct{}
func (w *WindowsFactory) CreateButton() Button { return &WindowsButton{} }
func (w *WindowsFactory) CreateCheckbox() Checkbox { return &WindowsCheckbox{} }
// ===== Mac 主题 =====
type MacButton struct{}
func (m *MacButton) Render() string { return "[Mac 风格按钮 · 圆角]" }
type MacCheckbox struct{}
func (m *MacCheckbox) Toggle() string { return "[Mac 复选框: ✓ · 半透明]" }
type MacFactory struct{}
func (m *MacFactory) CreateButton() Button { return &MacButton{} }
func (m *MacFactory) CreateCheckbox() Checkbox { return &MacCheckbox{} }
使用方式
func BuildUI(factory UIFactory) {
btn := factory.CreateButton()
cb := factory.CreateCheckbox()
fmt.Println(btn.Render())
fmt.Println(cb.Toggle())
}
// 一键切换整套主题
BuildUI(&WindowsFactory{}) // 全部 Windows 风格
BuildUI(&MacFactory{}) // 全部 Mac 风格
三种工厂模式对比
| 模式 | 创建目标 | 扩展方式 | 适用场景 |
|---|---|---|---|
| 简单工厂 | 单一产品 | 改工厂代码 | 产品种类少,创建逻辑简单 |
| 工厂方法 | 单一产品 | 新增工厂类 | 产品种类多,需要灵活扩展 |
| 抽象工厂 | 一组相关产品 | 新增工厂类 | 产品族(如不同主题、不同数据库) |
五、单例模式(Singleton)
问题场景
有些对象在整个程序生命周期中只需要一个实例:数据库连接池、配置管理器、日志记录器。如果到处 new,不仅浪费资源,还可能导致状态不一致。
单例模式保证一个类只有一个实例,并且提供全局访问点。
Go 语言的实现方式
Go 没有类的概念,但可以通过包级别的私有变量 + 公开的获取函数来实现单例。关键是要保证并发安全。
方式一:sync.Once(推荐)
package db
import (
"database/sql"
"sync"
)
var (
instance *sql.DB
once sync.Once
)
// GetDB 返回唯一的数据库连接实例
func GetDB() *sql.DB {
once.Do(func() {
// 这段代码只会执行一次,无论多少个 goroutine 同时调用
var err error
instance, err = sql.Open("mysql", "user:pass@tcp(localhost:3306)/mydb")
if err != nil {
panic(err)
}
})
return instance
}
sync.Once 的 Do 方法保证传入的函数只执行一次,哪怕被多个 goroutine 并发调用。内部实现用了原子操作做快速路径检查 + 互斥锁做慢路径保护,性能极好。
方式二:init 函数(饿汉式)
如果确定程序启动时就需要这个实例,可以用 init() 函数:
var instance *Config
func init() {
instance = &Config{
Host: "localhost",
Port: 8080,
}
}
func GetConfig() *Config {
return instance
}
init() 在包被导入时自动执行,且 Go 保证它在 main() 之前完成。这被称为"饿汉式"——不管用不用,先创建好。
方式三:互斥锁(懒汉式,不推荐)
var (
instance *Logger
mu sync.Mutex
)
func GetLogger() *Logger {
if instance == nil {
mu.Lock()
defer mu.Unlock()
if instance == nil { // 双重检查
instance = &Logger{}
}
}
return instance
}
这种双重检查锁定(DCL)在 Java 里很经典,但在 Go 里直接用 sync.Once 就行了——更简洁、更安全。
单例的注意事项
- 不要滥用:单例本质上是全局变量,过度使用会让代码难以测试(单例持有状态,测试之间会互相干扰)。
- 考虑依赖注入:很多时候,把依赖通过参数传入比用单例全局访问更清晰。
- 单例 ≠ 线程安全:单例本身只保证"只有一个实例",实例内部的数据结构是否线程安全是另一回事。
六、Go 中设计模式的哲学
Go 的设计模式不像 Java 那样有一套严格的"类图 + 继承体系"。Go 的核心工具就三个:
- 接口(interface):定义行为契约,解耦调用方和实现方。
- 组合(embedding):代替继承,通过嵌套结构体复用代码。
- 包(package):提供封装边界,包级别的私有变量 + 公开函数天然适合单例。
很多在 Java 里需要大量样板代码的模式,在 Go 里几行就能搞定。比如"策略模式"在 Go 里就是一个函数类型的参数,"装饰器模式"就是函数包装函数。
理解设计模式的核心思想(而不是死记 UML 图),然后自然地用 Go 的方式表达出来——这才是学设计模式的正确姿势。
七、关键要点
- 简单工厂:一个函数根据参数返回不同产品,适合产品种类少的情况。
- 工厂方法:把创建逻辑下放到子工厂,新增产品只需新增工厂类。
- 抽象工厂:创建一组相关的产品,保证产品族的一致性。
- 单例:Go 中用
sync.Once实现最优雅,init()适合启动时就需要初始化的场景。 - 不要过度设计:如果只有两三种产品,一个简单工厂就够了,没必要上工厂方法。设计模式是解决问题的工具,不是炫技的道具。
八、总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。
