Go语言设计模式入门:简单工厂和单例怎么用?

 更新时间:2026年07月29日 09:20:44   作者:FfHUCisI  
还在为Go语言中对象创建和全局实例管理发愁?本文用支付系统、UI主题等实战案例,手把手教你简单工厂、工厂方法、抽象工厂和单例模式的Go语言实现,了解如何用接口和sync.Once写出更简洁优雅的代码,避免过度设计,提升开发效率

一、设计模式是干什么的

写代码久了你会发现,很多问题在反复出现:怎么保证全局只有一个数据库连接?怎么根据配置创建不同类型的对象?怎么一步步构建一个参数多到令人发指的结构体?

前辈们把这些问题的最佳解法总结成了设计模式——不是死板的代码模板,而是一种"遇到这种情况就这样处理"的经验结晶。

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.OnceDo 方法保证传入的函数只执行一次,哪怕被多个 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 就行了——更简洁、更安全。

单例的注意事项

  1. 不要滥用:单例本质上是全局变量,过度使用会让代码难以测试(单例持有状态,测试之间会互相干扰)。
  2. 考虑依赖注入:很多时候,把依赖通过参数传入比用单例全局访问更清晰。
  3. 单例 ≠ 线程安全:单例本身只保证"只有一个实例",实例内部的数据结构是否线程安全是另一回事。

六、Go 中设计模式的哲学

Go 的设计模式不像 Java 那样有一套严格的"类图 + 继承体系"。Go 的核心工具就三个:

  1. 接口(interface):定义行为契约,解耦调用方和实现方。
  2. 组合(embedding):代替继承,通过嵌套结构体复用代码。
  3. 包(package):提供封装边界,包级别的私有变量 + 公开函数天然适合单例。

很多在 Java 里需要大量样板代码的模式,在 Go 里几行就能搞定。比如"策略模式"在 Go 里就是一个函数类型的参数,"装饰器模式"就是函数包装函数。

理解设计模式的核心思想(而不是死记 UML 图),然后自然地用 Go 的方式表达出来——这才是学设计模式的正确姿势。

七、关键要点

  1. 简单工厂:一个函数根据参数返回不同产品,适合产品种类少的情况。
  2. 工厂方法:把创建逻辑下放到子工厂,新增产品只需新增工厂类。
  3. 抽象工厂:创建一组相关的产品,保证产品族的一致性。
  4. 单例:Go 中用 sync.Once 实现最优雅,init() 适合启动时就需要初始化的场景。
  5. 不要过度设计:如果只有两三种产品,一个简单工厂就够了,没必要上工厂方法。设计模式是解决问题的工具,不是炫技的道具。

八、总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Go语言通过TCP协议实现聊天室功能

    Go语言通过TCP协议实现聊天室功能

    这篇文章主要为大家详细介绍了Go语言中如何通过TCP协议实现聊天室功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2024-04-04
  • go语言通过反射获取和设置结构体字段值的方法

    go语言通过反射获取和设置结构体字段值的方法

    这篇文章主要介绍了go语言通过反射获取和设置结构体字段值的方法,实例分析了Go语言反射的使用技巧,需要的朋友可以参考下
    2015-03-03
  • 基于Golang实现Excel表格的导入导出功能

    基于Golang实现Excel表格的导入导出功能

    最近项目开发中有涉及到Excel的导入与导出功能,特别是导出表格时需要特定的格式,所以本文给大家介绍了基于Golang实现Excel表格的导入导出功能,文中通过代码示例和图文介绍的非常详细,需要的朋友可以参考下
    2023-12-12
  • Go中各种newreader和newbuffer的使用总结

    Go中各种newreader和newbuffer的使用总结

    这篇文章主要为大家详细介绍了Go语言中各种newreader和newbuffer的使用的相关资料,文中的示例代码讲解详细,具有一定的参考价值,感兴趣的小伙伴可以了解下
    2023-11-11
  • 解决golang时间字符串转time.Time的坑

    解决golang时间字符串转time.Time的坑

    这篇文章主要介绍了解决golang时间字符串转time.Time的坑,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04
  • 使用Go语言简单模拟Python的生成器

    使用Go语言简单模拟Python的生成器

    这篇文章主要介绍了使用Go语言简单模拟Python的生成器,Python的generator是非常酷的功能,用Go实现的代码也较为简洁,需要的朋友可以参考下
    2015-08-08
  • Golang中sync.RWMutex 读写锁的实现

    Golang中sync.RWMutex 读写锁的实现

    本文主要介绍了RWMutex如何通过允许多个读操作同时进行、仅在写入时互斥,解决Mutex的读写全互斥瓶颈,具有一定的参考价值,感兴趣的可以了解一下
    2026-07-07
  • Go gin框架加载Html模板文件的方法

    Go gin框架加载Html模板文件的方法

    这篇文章主要介绍了Go gin框架加载Html模板文件的方法,Gin框架没有内置静态文件服务,但可以使用gin.Static或gin.StaticFS中间件来提供静态文件服务,文中有相关的代码示例供大家参考,需要的朋友可以参考下
    2024-03-03
  • golang中使用proto3协议导致的空值字段不显示的问题处理方案

    golang中使用proto3协议导致的空值字段不显示的问题处理方案

    这篇文章主要介绍了golang中使用proto3协议导致的空值字段不显示的问题处理方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-10-10
  • 深入理解Golang make和new的区别及实现原理

    深入理解Golang make和new的区别及实现原理

    在Go语言中,有两个比较雷同的内置函数,分别是new和make方法,二者都可以用来分配内存,那他们有什么区别呢?下面我们就从底层来分析一下二者的不同。感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助
    2022-10-10

最新评论