和传统的 GOF, Java, C# 教科书式的 设计模式 不同,Go 语言设计从一开始就力求简洁,有其他编程语言基础的读者在学习和使用 Go 语言时, 万万不可按图索骥、生搬硬套,简单的事情复杂化。本文带领大家一起看一下,Go 语言标准库中自带的 编程设计模式。
本文转载自微信公众号「洋芋编程」,作者蛮荆 。转载本文请联系洋芋编程公众号。
在软件工程中,设计模式(design pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案。 -- 维基百科
和传统的 GOF, Java, C# 教科书式的 设计模式 不同,Go 语言设计从一开始就力求简洁,有其他编程语言基础的读者在学习和使用 Go 语言时, 万万不可按图索骥、生搬硬套,简单的事情复杂化。
本文带领大家一起看一下,Go 语言标准库中自带的 编程设计模式。
单例模式 确保一个类只有一个实例,并提供对该实例的全局访问。
通过使用标准库中的 sync.Once 对业务对象进行简单封装,即可实现 单例模式,简单安全高效。
复制 package main
import "sync"
var (
once sync.Once
instance Singleton
)
// Singleton 业务对象
type Singleton struct {
}
// NewInstance 单例模式方法
func NewInstance( ) Singleton {
once.Do ( func( ) {
instance = Singleton{ }
} )
return instance
}
func main( ) {
// 调用方代码
s1 : = NewInstance( )
s2 : = NewInstance( )
s3 : = NewInstance( )
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27.
Go 标准库单例模式
简单工厂模式 Go 语言本身没有 构造方法 特性,工程实践中一般使用 NewXXX 创建新的对象 (XXX 为对象名称),比如标准库中的:
复制 // errors/ errors.go
func New( text string) error {
return & errorString{ text }
}
// sync/ cond.go
func NewCond( l Locker) * Cond {
return & Cond{ L: l}
}
在这个基础上,如果方法返回的是 interface 的时候,其实就等于是 简单工厂模式,然后再加一层抽象的话,就接近于 抽象工厂模式。
复制 package main
// ConfigParser 配置解析接口
type ConfigParser interface {
Parse( p [ ] byte)
}
// JsonParser Json 文件解析器
type JsonParser struct {
}
func ( j * JsonParser) Parse( p [ ] byte) {
}
func newJsonParser( ) * JsonParser {
return & JsonParser{ }
}
// YamlParser Yaml 文件解析器
type YamlParser struct {
}
func ( y * YamlParser) Parse( p [ ] byte) {
}
func newYamlParser( ) * YamlParser {
return & YamlParser{ }
}
type ConfigType uint8
const (
JsonType ConfigType = 1 << iota
YamlType
)
// NewConfig 根据不同的类型创建对应的解析器
func NewConfig( t ConfigType) ConfigParser {
switch t {
case JsonType:
return newJsonParser( )
case YamlType:
return newYamlParser( )
default:
return nil
}
}
func main( ) {
// 调用方代码
jsonParser : = NewConfig( JsonType)
yamlParser : = NewConfig( YamlType)
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55.
Go 实现简单工厂模式
对象池模式 通过回收利用对象避免获取和释放资源所需的昂贵成本,我们可以直接使用 sync.Pool 对象来实现功能。
复制 package main
import (
"net/http"
"sync"
)
var (
// HTTP Request 对象池
reqPool = sync.Pool {
New: func( ) any {
return http.Request { }
} ,
}
)
func main( ) {
// 调用方代码
r1 : = reqPool.Get ( )
r2 : = reqPool.Get ( )
r3 : = reqPool.Get ( )
reqPool.Put ( r1)
reqPool.Put ( r2)
reqPool.Put ( r3)
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26.
构建模式 (Builder) 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
如果用传统的方法实现 构建模式,对应的 Go 语言代码大致是下面这个样子:
复制 package main
type QueryBuilder interface {
Select ( table string, columns [ ] string) QueryBuilder
Where ( conditions ...string) QueryBuilder
GetRawSQL( ) string
}
type MySQLQueryBuilder struct {
}
func ( m * MySQLQueryBuilder) Select ( table string, columns ...string) QueryBuilder {
// 具体实现代码跳过
return nil
}
func ( m * MySQLQueryBuilder) Where ( conditions ...string) QueryBuilder {
// 具体实现代码跳过
return nil
}
func ( m * MySQLQueryBuilder) GetRawSQL( ) string {
// 具体实现代码跳过
return ""
}
func main( ) {
// 调用方代码
m : = & MySQLQueryBuilder{ }
sql : = m.Select ( "users" , "username" , "password" ) .
Where ( "id = 100" ) .
GetRawSQL( )
println( sql)
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36.
Go 实现构建模式
上面的代码中,通过经典的链式调用来构造出具体的 SQL 语句,但是在 Go 语言中,我们一般使用另外一种模式来实现同样的功能 FUNCTIONAL OPTIONS, 这似乎也是 Go 语言中最流行的模式之一。
复制 package main
type SQL struct {
Table string
Columns [ ] string
Where [ ] string
}
type Option func( s * SQL)
func Table ( t string) Option {
// 注意返回值类型
return func( s * SQL) {
s.Table = t
}
}
func Columns( cs ...string) Option {
// 注意返回值类型
return func( s * SQL) {
s.Columns = cs
}
}
func Where ( conditions ...string) Option {
// 注意返回值类型
return func( s * SQL) {
s.Where = conditions
}
}
func NewSQL( options ...Option) * SQL {
sql : = & SQL{ }
for _, option : = range options {
option( sql)
}
return sql
}
func main( ) {
// 调用方代码
sql : = NewSQL( Table ( "users" ) ,
Columns( "username" , "password" ) ,
Where ( "id = 100" ) ,
)
println( sql)
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50.
Go FUNCTIONAL OPTIONS 模式
观察者模式 在对象间定义一个一对多的联系性,由此当一个对象改变了状态,所有其他相关的对象会被通知并且自动刷新。
如果用传统的方法实现 观察者模式,对应的 Go 语言代码大致是下面这个样子:
复制 package main
import "math"
// Observer 观察者接口
type Observer interface {
OnNotify( Event)
}
// Notifier 订阅接口
type Notifier interface {
Register( Observer)
Deregister( Observer)
Notify( Event)
}
type (
Event struct {
Data int64
}
eventObserver struct {
id int
}
eventNotifier struct {
observers map[ Observer] struct{ }
}
)
// OnNotify 观察者收到订阅的时间回调
func ( o * eventObserver) OnNotify( e Event) {
}
// Register 注册观察者
func ( o * eventNotifier) Register( l Observer) {
o.observers [ l] = struct{ } { }
}
// Deregister 移除观察者
func ( o * eventNotifier) Deregister( l Observer) {
delete ( o.observers , l)
}
// Notify 发出通知
func ( o * eventNotifier) Notify( e Event) {
for p : = range o.observers {
p.OnNotify ( e)
}
}
func main( ) {
// 调用方代码
notifier : = eventNotifier{
observers: make( map[ Observer] struct{ } ) ,
}
notifier.Register ( & eventObserver{ 1 } )
notifier.Register ( & eventObserver{ 2 } )
notifier.Register ( & eventObserver{ 3 } )
notifier.Notify ( Event{ Data: math.MaxInt64 } )
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63.
Go 实现观察者模式
但其实我们有更简洁的方法,直接使用标准库中的 sync.Cond 对象,改造之后的 观察者模式 代码大概是这个样子:
复制 package main
import (
"fmt"
"sync"
"time"
)
var done = false
func read( name string, c * sync.Cond ) {
fmt.Println ( name, "starts reading" )
c.L .Lock ( )
for ! done {
c.Wait ( ) // 等待发出通知
}
c.L .Unlock ( )
}
func write( name string, c * sync.Cond ) {
fmt.Println ( name, "starts writing" )
time .Sleep ( 100 * time .Millisecond )
c.L .Lock ( )
done = true // 设置条件变量
c.L .Unlock ( )
fmt.Println ( name, "wakes all" )
c.Broadcast ( ) // 通知所有观察者
}
func main( ) {
cond : = sync.NewCond ( & sync.Mutex { } ) // 创建时传入一个互斥锁
// 3 个观察者
go read( "reader1" , cond)
go read( "reader2" , cond)
go read( "reader3" , cond)
time .Sleep ( time .Second ) // 模拟延时
write( "writer-1" , cond) // 发出通知
time .Sleep ( time .Second ) // 模拟延时
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46.
Go 标准库观察者模式
将代码改造为 sync.Cond 之后,代码量更好,结构更简洁。
ok/error 模式 在 Go 语言中,经常在一个表达式返回 2 个参数时使用这种模式:
第 1 个参数是一个值或者 nil 第 2 个参数是 true/false 或者 error 在一个需要赋值的 if 条件语句中,使用这种模式去检测第 2 个参数值会让代码显得优雅简洁。
在函数返回时检测错误
复制 package main
func foo( ) ( int , error) {
return 0 , nil
}
func main( ) {
if v, err : = foo( ) ; err != nil {
panic( err)
} else {
println( v)
}
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13.
检测 map 是否存在指定的 key
复制 package main
func main( ) {
m : = make( map[ int ] string)
if v, ok : = m[ 0 ] ; ok {
println( v)
}
}
类型断言
复制 package main
func foo( ) interface{ } {
return 1024
}
func main( ) {
n : = foo( )
if v, ok : = n.( int ) ; ok {
println( v)
}
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12.
检测通道是否关闭
复制 package main
func main( ) {
ch : = make( chan int )
go func( ) {
for i : = 0 ; i < 5 ; i++ {
ch <- i
}
close( ch)
} ( )
for {
if v, ok : = <- ch; ok {
println( v)
} else {
return
}
}
}
// $ go run main.go
// 输出如下
// 0
// 1
// 2
// 3
// 4
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28.
附加内容 闭包 有时候,我们可以利用 闭包 实现一些短小精悍的内部函数。
计数器
复制 package main
func main( ) {
newSeqInc : = func( ) func( ) int {
seq : = 0
return func( ) int {
seq++
return seq
}
}
seq : = newSeqInc( ) // 创建一个计数器
println( seq( ) ) // 1
println( seq( ) ) // 2
println( seq( ) ) // 3
seq2 : = newSeqInc( ) // 创建另一个计数器
println( seq2( ) ) // 1
println( seq2( ) ) // 2
println( seq2( ) ) // 3
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21.
小结 下面表格列出了常用的 设计模式,其中 Go 标准库自带的 模式 已经用删除线标识,读者可以和自己常用的 设计模式 进行对比。
创建型模式
结构性模式
行为型模式
单例
适配器
策略
简单工厂
装饰者
观察者
抽象工厂
代理
状态
对象池
责任链
构建
长期以来,设计模式 一直处于尴尬的位置:初学者被各种概念和关系搞得不知所云,有经验的程序员会觉得 “这种代码写法 (这里指设计模式),我早就知道了啊”。 鉴于这种情况,本文中没有涉及到的 设计模式,笔者不打算再一一描述,感兴趣的读者可以直接跳到 仓库代码[1] 查看示例代码。
相比于设计模式,更重要的是理解语言本身的特性以及最佳实践。
扩展阅读 Go 与面向对象 设计模式 - 维基百科[2] go-examples-for-beginners/patterns[3] 圣杯与银弹 · 没用的设计模式[4] tmrts/go-patterns[5] DESIGN PATTERNS in GO[6] 解密“设计模式”[7] Go 编程模式 - 酷壳[8] 引用链接 [1] 仓库代码: https://github.com/duanbiaowu/go-examples-for-beginners/tree/master/patterns
[2] 设计模式 - 维基百科: https://zh.wikipedia.org/wiki/设计模式_(计算机)
[3] go-examples-for-beginners/patterns: https://github.com/duanbiaowu/go-examples-for-beginners/tree/master/patterns
[4] 圣杯与银弹 · 没用的设计模式: https://draveness.me/holy-grail-design-pattern/
[5] tmrts/go-patterns: https://github.com/tmrts/go-patterns
[6] DESIGN PATTERNS in GO: https://refactoring.guru/design-patterns/go
[7] 解密“设计模式”: http://www.yinwang.org/blog-cn/2013/03/07/design-patterns
[8] Go 编程模式 - 酷壳: https://coolshell.cn/articles/series/go%e7%bc%96%e7%a8%8b%e6%a8%a1%e5%bc%8f