Go常用的设计模式
常见的设计模式,如 单例模式 、工厂模式 、策略模式 、观察者模式 、代理模式 、装饰器模式 和 适配器模式 都可以在 Go 中实现,适用于不同的开发需求。
这些设计模式不仅能帮助你编写结构清晰、可维护的代码,还能让你更好地应对复杂的编程问题。
一、单例模式(Singleton)
简介:
Go 的单例模式(Singleton Pattern)适用于某些需要确保一个类(或结构体)在整个应用程序中只有一个实例的场景。通常情况下,单例模式用于全局共享资源、缓存、日志管理、数据库连接等场景,避免了不必要的对象创建和资源浪费。
使用场景:
- 配置管理
- 日志管理
- 数据连接池
- 缓存管理
- 线程安全的全局状态管理
- 系统资源管理
优点:
- 线程安全:使用
sync.Once
可以确保在并发环境下,单例对象只被创建一次,避免竞态条件和数据竞争。 - 延迟初始化(懒汉式):只有在首次调用时才进行实例化,节省内存和启动时间。
- 全局唯一性:单例模式可以确保在整个程序生命周期中只有一个实例,适合需要共享资源的场景(如配置管理、日志记录)。
- 节省资源:由于实例只创建一次,避免频繁创建和销毁带来的性能瓶颈。
- 方便访问:单例模式提供一个全局访问点,通过调用同一个函数即可获取实例,方便在整个程序中使用。
缺点:
- 全局状态导致代码难以测试:单例模式带来的全局状态使得单元测试难以编写,尤其是需要模拟或替换单例时。
- 难以扩展:单例模式将创建逻辑硬编码唉单例类中,如果需要更改创建逻辑,往往需要修改核心代码,违反开闭原则。
- 隐藏依赖性:使用单例时,其他模块可能隐式依赖于单例对象,在修改单例时容易引发不可预期的问题。
- 不利于并行测试:由于单例模式在程序中只有一个实例,多个测试用例无法并发运行,可能产生数据污染。
- 潜在内存泄露:如果单例中持有大量资源且没有及时释放,可能导致内存泄漏。
实现:
go
package main
import (
"fmt"
"sync"
)
type Singleton struct {
// 可能包含一些属性
}
var instance *Singleton
var once sync.Once
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{}
})
return instance
}
func main() {
s1 := GetInstance()
s2 := GetInstance()
fmt.Println(s1 == s2) // true
}
二、工厂模式(Factory)
简介:
工厂模式用于创建对象的实例,但不暴露具体的创建逻辑,它通过接口来解耦对象的创建和使用。
使用场景:
- 构建复杂对象:如果对象创建过程复杂且有多种变体,使用工厂可以贱货创建逻辑。
- 接口实例化:当代码中涉及多个实现相同接口的结构体时,使用工厂可以屏蔽具体实现,方便切换。
- 减少重复代码:如果对象创建涉及大量重复逻辑,工厂方法可以封装这些逻辑。
- 依赖注入(DI)和解耦:使用工厂可以避免代码中直接依赖具体实现类,提升代码灵活性。
- 面向接口编程:在某昔日场景下需要根据配置或运行时条件来创建不同的实例。
优点:
- 解耦性高:工厂方法将对象创建与对象使用分离,调用者无需关心对象的具体创建过程。
- 符合开闭原则:增加新产品时,只需增加新的工厂方法,而无需修改原有代码,减少了对现有代码的侵入。
- 增强代码复用性:工厂方法可以复用已有的创建逻辑,避免代码重复。
- 代码结构清晰:通过工厂模式管理对象创建逻辑,使代码结构更加清晰和规范。
- 便于扩展和维护:由于工厂模式使用接口和多态特性,当需要新增类型时,不影响原有代码。
缺点:
- 复杂性增加:简单场景下使用工厂模式多此一举,反而增加了代码复杂度。
- 不适用于简单对象:如果对象创建很简单,工厂模式会引入不必要的抽象,降低代码可读性。
- 难以追踪:工厂方法隐藏了具体实现,可能让代码调用链变长,增加调试难度。
- 类爆炸问题:如果产品种类繁多,则每种产品都需要一个对应的工厂类,导致类的数量大幅增加。
实现:
简单工厂模式:适用对象较少创建、逻辑简单的场景。
go
package main
import (
"fmt"
)
// 通知接口
type Notifier interface {
Notify(message string)
}
// 邮件通知
type EmailNotifier struct{}
func (e *EmailNotifier) Notify(message string) {
fmt.Println("Email Notification:", message)
}
// 短信通知
type SMSNotifier struct{}
func (s *SMSNotifier) Notify(message string) {
fmt.Println("SMS Notification:", message)
}
// 工厂函数
func NewNotifier(notifyType string) Notifier {
switch notifyType {
case "email":
return &EmailNotifier{}
case "sms":
return &SMSNotifier{}
default:
return nil
}
}
func main() {
notifier := NewNotifier("email")
if notifier != nil {
notifier.Notify("Hello via Email!")
}
notifier = NewNotifier("sms")
if notifier != nil {
notifier.Notify("Hello via SMS!")
}
}
三、策略模式(Strategy Pattern)
简介:
一种行为设计模式,旨在将一组算法封装到独立的类中,使它们可以互相替换,通过使用策略模式,算法的变化不会影响使用算法的上下文代码。策略模式在Go语言中尤为常见,因为接口和结构体组合的特性使得实现即灵活又高效。
使用场景:
- 算法族:有多个可替换算法的场景,例如加密、排序。
- 业务规则变化频繁:例如支付策略、折扣策略。
- 避免使用条件语句:如大量
if-else
或switch
判断的地方。
优点:
- 灵活性:可以在运行时动态更改策略。
- 代码复用:不同算法独立封装,避免了条件语句的堆叠。
- 遵循开闭原则:添加新策略时无需修改上下文类。
缺点:
- 复杂性:每个策略需要创建一个类或结构体,增加代码量。
- 策略暴漏:客户端需要知道有哪些策略才能进行选择。
实现:
go
package main
import "fmt"
// 策略接口
type Strategy interface {
Execute(a, b int) int
}
// 加法策略
type AddStrategy struct{}
func (s AddStrategy) Execute(a, b int) int {
return a + b
}
// 乘法策略
type MultiplyStrategy struct{}
func (s MultiplyStrategy) Execute(a, b int) int {
return a * b
}
// 上下文结构体
type Context struct {
strategy Strategy
}
// 设置策略
func (c *Context) SetStrategy(strategy Strategy) {
c.strategy = strategy
}
// 执行策略
func (c *Context) ExecuteStrategy(a, b int) int {
return c.strategy.Execute(a, b)
}
func main() {
context := Context{}
// 使用加法策略
context.SetStrategy(AddStrategy{})
result := context.ExecuteStrategy(5, 3)
fmt.Println("加法策略结果:", result) // 输出:8
// 使用乘法策略
context.SetStrategy(MultiplyStrategy{})
result = context.ExecuteStrategy(5, 3)
fmt.Println("乘法策略结果:", result) // 输出:15
}
四、观察者模式(Observer Pattern)
简介:
一种行为设计模式,允许对象在其状态发生更改时通知其他依赖对象。它定义了一种一对多的依赖关系,一个对象(主题/被观察者)状态变化时,所有依赖者(观察者)都会收到通知并自动更新。
使用场景:
- 事件驱动系统:如GUI事件监听、消息广播系统。
- 订阅-发布系统:如新闻发布、股票行情更新。
- 监控系统:如服务健康状态监控和报警。
优点:
- 解耦:主题和观察者之间的耦合性低,便于独立扩展。
- 灵活性:可以在运行时动态添加和删除观察者。
- 实时更新:状态变化时自动通知观察者,符合实时性需求。
缺点:
- 通知滞后:当观察者数量较多时,通知操作可能会有一定延迟。
- 内存泄露风险:如果没有正确管理观察者的注销,可能会导致内存泄漏。
- 调试难度较大:链式调用可能增加排查时问题的复杂性。
实现:
go
package main
import "fmt"
// 观察者接口
type Observer interface {
Update(message string)
}
// 主题接口
type Subject interface {
Register(observer Observer)
Unregister(observer Observer)
NotifyAll(message string)
}
// 具体主题
type NewsPublisher struct {
observers []Observer
}
// 注册观察者
func (n *NewsPublisher) Register(observer Observer) {
n.observers = append(n.observers, observer)
}
// 注销观察者
func (n *NewsPublisher) Unregister(observer Observer) {
for i, obs := range n.observers {
if obs == observer {
n.observers = append(n.observers[:i], n.observers[i+1:]...)
break
}
}
}
// 通知所有观察者
func (n *NewsPublisher) NotifyAll(message string) {
for _, observer := range n.observers {
observer.Update(message)
}
}
// 具体观察者
type NewsSubscriber struct {
name string
}
// 接收更新通知
func (n *NewsSubscriber) Update(message string) {
fmt.Printf("[%s] 收到新闻更新:%s\n", n.name, message)
}
// 创建新的观察者
func NewSubscriber(name string) *NewsSubscriber {
return &NewsSubscriber{name: name}
}
func main() {
// 创建新闻发布者(主题) 隐式使用
// var publisher Subject = &NewsPublisher{}
publisher := &NewsPublisher{}
// 创建观察者(订阅者)
sub1 := NewSubscriber("Alice")
sub2 := NewSubscriber("Bob")
sub3 := NewSubscriber("Charlie")
// 注册观察者
publisher.Register(sub1)
publisher.Register(sub2)
publisher.Register(sub3)
// 发布新闻更新
publisher.NotifyAll("Go 1.21 发布了!")
// 注销一个观察者
publisher.Unregister(sub2)
// 再次发布新闻
publisher.NotifyAll("Go 1.22 即将发布!")
}
五、代理模式(Proxy Pattern)
简介:
一种结构型设计模式,它通过一个代理对象来控制对目标对象的访问。代理对象可以在客户端和真实对象之间进行一些操作,比如权限控制、懒加载、日志记录、缓存等,特别适合增强现有类的功能而无需修改原有代码。
使用场景:
- 远程代理:使用代理来控制对远程服务的访问。
- 虚拟代理:延迟初始化较为复杂的对象。
- 安全代理:检查权限,只有合法用户才能访问。
- 只能引用代理:自动进行医用计数和资源管理。
优点:
- 职责分离:代理对象负责处理非核心业务(如日志记录),核心业务由真实对象完成。
- 灵活性强:可以动态地将额外操作附加到真实对象上。
- 曾强功能:在不修改原始类的情况下增加新功能。
缺点:
- 开销增加:由于增加了代理对象,性能有一定损耗。
- 代码复杂:增加了代码复杂性和维护难度。
实现:
go
package main
import (
"fmt"
"time"
)
// 抽象接口(Subject)
type BankAccount interface {
Deposit(amount float64)
Withdraw(amount float64)
GetBalance() float64
}
// 实际对象(RealSubject):银行账户
type RealBankAccount struct {
balance float64
}
func (r *RealBankAccount) Deposit(amount float64) {
r.balance += amount
fmt.Printf("存入:%.2f 元,当前余额:%.2f 元\n", amount, r.balance)
}
func (r *RealBankAccount) Withdraw(amount float64) {
if amount > r.balance {
fmt.Println("余额不足,取款失败!")
return
}
r.balance -= amount
fmt.Printf("取出:%.2f 元,当前余额:%.2f 元\n", amount, r.balance)
}
func (r *RealBankAccount) GetBalance() float64 {
return r.balance
}
// 代理对象(Proxy):日志代理
type LoggingProxy struct {
realAccount BankAccount
}
func NewLoggingProxy(realAccount BankAccount) *LoggingProxy {
return &LoggingProxy{realAccount: realAccount}
}
func (p *LoggingProxy) Deposit(amount float64) {
fmt.Printf("[%s] 正在进行存款操作...\n", time.Now().Format("2006-01-02 15:04:05"))
p.realAccount.Deposit(amount)
}
func (p *LoggingProxy) Withdraw(amount float64) {
fmt.Printf("[%s] 正在进行取款操作...\n", time.Now().Format("2006-01-02 15:04:05"))
p.realAccount.Withdraw(amount)
}
func (p *LoggingProxy) GetBalance() float64 {
balance := p.realAccount.GetBalance()
fmt.Printf("[%s] 查询余额:%.2f 元\n", time.Now().Format("2006-01-02 15:04:05"), balance)
return balance
}
// 客户端代码
func main() {
// 创建实际银行账户
realAccount := &RealBankAccount{}
// 使用代理来包装实际账户
proxy := NewLoggingProxy(realAccount)
// 通过代理进行操作
proxy.Deposit(1000)
proxy.Withdraw(300)
proxy.GetBalance()
}
六、装饰器模式
简介:
一种结构型设计模式,允许在不修改对象结构的情况下动态地为对象添加新功能。
使用场景:
- 功能增强:为对象动态添加功能。
- 替代子类继承:通过组合而非继承来扩展功能。
- 职责划分:使每个装饰器负责特定功能,符合单一职责原则。
装饰器模式的核心思想:
- 组件接口(Component):定义一个可以被装饰的对象接口。
- 具体组件(Concrete Component):实现基础功能。
- 装饰器接口(Decorator):持有组件接口的引用,且具有相同的方法。
- 具体装饰器(Concrete Decorator):扩展组件的功能。
优点:
- 灵活性高:可以通过多个装饰器动态组合新功能。
- 符合开闭原则:可以随时添加新装饰器而不影响原有代码。
- 职责单一:每个装饰器只负责一个特定功能。
缺点:
- 装饰链过长:可能导致结构复杂,难以维护。
- 性能开销:多层嵌套会带来性能损耗。
实现:
go
package main
import (
"fmt"
)
// Component 接口:咖啡饮品
type Beverage interface {
GetDescription() string
Cost() float64
}
// 具体组件:基础咖啡
type Espresso struct{}
func (e *Espresso) GetDescription() string {
return "Espresso"
}
func (e *Espresso) Cost() float64 {
return 15.0
}
// 装饰器基类:实现 Beverage 接口
type CondimentDecorator struct {
beverage Beverage
}
func (c *CondimentDecorator) GetDescription() string {
return c.beverage.GetDescription()
}
func (c *CondimentDecorator) Cost() float64 {
return c.beverage.Cost()
}
// 具体装饰器:牛奶
type Milk struct {
CondimentDecorator
}
func NewMilk(beverage Beverage) *Milk {
return &Milk{CondimentDecorator{beverage}}
}
func (m *Milk) GetDescription() string {
return m.beverage.GetDescription() + ", Milk"
}
func (m *Milk) Cost() float64 {
return m.beverage.Cost() + 3.5
}
// 具体装饰器:糖
type Sugar struct {
CondimentDecorator
}
func NewSugar(beverage Beverage) *Sugar {
return &Sugar{CondimentDecorator{beverage}}
}
func (s *Sugar) GetDescription() string {
return s.beverage.GetDescription() + ", Sugar"
}
func (s *Sugar) Cost() float64 {
return s.beverage.Cost() + 1.0
}
// 客户端代码
func main() {
// 创建基础咖啡
var beverage Beverage = &Espresso{}
fmt.Printf("饮品:%s,价格:%.2f 元\n", beverage.GetDescription(), beverage.Cost())
// 加牛奶
beverage = NewMilk(beverage)
fmt.Printf("饮品:%s,价格:%.2f 元\n", beverage.GetDescription(), beverage.Cost())
// 再加糖
beverage = NewSugar(beverage)
fmt.Printf("饮品:%s,价格:%.2f 元\n", beverage.GetDescription(), beverage.Cost())
}
装饰器模式和代理模式对比
特性 | 装饰器模式 | 代理模式 |
---|---|---|
主要目的 | 动态扩展功能 | 控制对对象的访问 |
结构特点 | 组合多个装饰器形成链 | 代理对象持有实际对象的引用 |
典型应用场景 | 日志、性能监控、权限校验、增强对象功能 | 远程代理、虚拟代理、安全代理、缓存代理 |
七、适配器模式(Adapter Pattern)
简介:
一种结构型设计模式,它通过两个不兼容的接口提供一个适配器,使得它们能够一起工作。适配器模式可以将一个接口转换为客户端期望的另一个接口,目的时让不兼容的接口能过够合作。
使用场景:
- 多重接口适配(统一接口标准):在一个系统中,你可能需要使用多个不兼容的接口,而这些接口都执行类似的操作。适配器模式可以帮助你统一这些接口,使得系统中其他部分可以通过相同的接口与它们交互。
- 兼容性问题(与现有系统兼容):如果你正在集成一个第三方库,或者使用一个遗留系统,而该系统的接口与现有系统不兼容,适配器模式可以帮助你转换这些不兼容的接口,使得它们能够顺利协作。
- 第三方库接口的适配(外部API整合):当使用第三方库时,这些库通常提供不同的接口,而你希望用一个统一的接口访问这些库。适配器模式可以将第三方库的接口适配为你项目中需要的标准接口。
- 接口升级或变化:当你需要对现有接口进行修改,但又不希望影响到客户端的代码时,适配器模式可以帮助你维护原接口的兼容性,同时在背后对接口进行改造或升级。
- 不同硬件或设备的适配:当你在开发跨平台应用或者硬件交互时,可能需要适配不同硬件或设备的接口。适配器模式可以帮助将不同硬件提供的接口适配到你应用需要的标准接口上。
- 系统迁移或重构:在系统重构的过程中,可能会涉及到接口的更改,而你又希望让现有代码与新代码兼容。适配器模式能够在过渡期间无缝衔接新旧系统的接口。
- 替代继承的场景(类适配):如果一个类不适用于继承的方式,或者你不想改变原有类的结构,可以使用适配器模式代替继承来扩展功能。
- API或数据协议的适配:不同的系统或组件之间可能使用不同的数据格式或协议,适配器模式能够在这些不同的格式之间提供一个桥梁。
适配器模式的核心思想
- 目标接口(Target):客户端期望使用的接口。
- 源接口(Adaptee):需要适配的现有接口,它的方法不能直接与客户端使用的接口兼容。
- 适配器(Adapter):将源接口转换为目标接口,使得客户端可以通过目标接口使用源接口的功能。
优点:
- 兼容不兼容的接口:使得本来不兼容的接口通过适配器能够协同工作。
- 符合开闭原则:通过适配器可以在不修改原有类的情况下,改变接口的使用方式。
- 解耦:客户端不需要知道适配器的实现,只需要依赖目标接口。
缺点:
- 增加代码复杂性:适配器模式会引入额外的类和对象,可能增加代码的复杂性。
- 性能开销:在适配器模式中,通常有额外的间接调用,可能导致轻微的性能损失。
实现:
go
package main
import "fmt"
// 目标接口(Target):要求的电源接口
type PowerOutlet interface {
SupplyPower() string
}
// 源接口(Adaptee):我们现有的电源接口
type TwoPinSocket struct{}
func (s *TwoPinSocket) ProvidePower() string {
return "提供 220V 电流"
}
// 适配器(Adapter):将现有电源接口转换为目标接口
type Adapter struct {
socket *TwoPinSocket
}
// 适配器的方法:使其实现目标接口
func (a *Adapter) SupplyPower() string {
return a.socket.ProvidePower()
}
// 客户端代码
func main() {
// 使用现有的 2 针电源插座(不符合目标接口)
twoPinSocket := &TwoPinSocket{}
// 通过适配器将其转换为目标接口
adapter := &Adapter{socket: twoPinSocket}
// 客户端通过目标接口使用适配后的电源
fmt.Println("设备电源:", adapter.SupplyPower())
}
适配器模式与代理模式的对比
特性 | 适配器模式 | 代理模式 |
---|---|---|
主要目的 | 使接口兼容并进行转换,适配不同接口的类 | 控制对目标对象的访问,通常是延迟或虚拟化操作。 |
结构特点 | 客户端和目标接口之间通过适配器进行转换 | 代理对象持有实际对象的引用,进行控制访问 |
典型应用场景 | 使得不兼容的类能够协作,转换接口 | 控制对实际对象的访问(如延迟加载、远程调用等) |