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()
}
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)
}
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)
}
構建模式 (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)
}
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)
}
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})
}
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) // 模擬延時
}
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)
}
}
檢測 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)
}
}
檢測通道是否關閉
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
附加內容
閉包
有時候,我們可以利用 閉包
實現一些短小精悍的內部函數。
計數器
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
}
小結
下面表格列出了常用的 設計模式
,其中 Go 標準庫自帶的 模式
已經用刪除線標識,讀者可以和自己常用的 設計模式
進行對比。
長期以來,設計模式
一直處於尷尬的位置:初學者被各種概念和關係搞得不知所云,有經驗的程序員會覺得 “這種代碼寫法 (這裏指設計模式),我早就知道了啊”。 鑑於這種情況,本文中沒有涉及到的 設計模式
,筆者不打算再一一描述,感興趣的讀者可以直接跳到 倉庫代碼 [1] 查看示例代碼。
相比於設計模式,更重要的是理解語言本身的特性以及最佳實踐。
擴展閱讀
-
設計模式 - 維基百科 [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/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F_(%E8%AE%A1%E7%AE%97%E6%9C%BA)
[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
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/qTYlHqozkWHkhF9OKfaAMQ