Skip to main content
  1. Docs/

Golang 设计模式

·5 mins· ·
Golang
Owl Dawn
Author
Owl Dawn
Table of Contents

单例模式
#

双重检查锁版本的单例模式:

// singleton v0
type Singleton struct{}

var (
   instance *Singleton
   lock     sync.Mutex
)

func GetInstance() *Singleton {
   if instance == nil {
      lock.Lock()
      defer lock.Unlock()
      if instance == nil {
         instance = &Singleton{}
      }
   }
   return instance
}

存在的问题

  • 指令重排 instance = &Singleton{x=1} 这个语句,包括分配内存内存初始化指针赋值三个操作,这里内存初始化和指针赋值可能会发生指令重排序,可能导致返回的 instance 没有完成内存初始化,而其他线程就会获得一个没有完成初始化的对象
  • 原子性 golang 一般的数据结构是不保证原子性的,除了 atomic 包提供的原子操作。因此 if instance == nil 这个读操作是没法保证原子性,可能返回一个半初始化的指针.

解决

  • 读写锁

    用一把读写锁来保证读取 instance 的操作不会被指令重排序和非原子操作所影响。

    type Singleton struct{}
    
    var (
        instancev1 *Singleton
        lockv1     sync.RWMutex
    )
    
    func GetInstancev1() *Singleton {
        lockv1.RLock()
        ins := instancev1
        lockv1.RUnlock()
        if ins == nil {
            lockv1.Lock()
            defer lockv1.Unlock()
            if instancev1 == nil {
                instancev1 = &Singleton{}
            }
        }
        return instancev1
    }
    
  • atomic

    用原子赋值保证只有当 instance 指针被完整赋值以后,才能被读到:

    type Singleton struct{}
    
    var (
        instancev2 *Singleton
        lockv2     sync.Mutex
        done       uint32
    )
    
    func GetInstancev2() *Singleton {
        if atomic.LoadUint32(&done) == 0 {
            lockv2.Lock()
            defer lockv2.Unlock()
            if done == 0 {
                defer atomic.StoreUint32(&done, 1)
                instancev2 = &Singleton{}
            }
        }
        return instancev2
    }
    
  • sync.Once

    实际上 golang 标准库里的 sync.Once 就是采用 atomic 实现的双重检查锁,所以 golang 里的最佳实践是直接使用 sync.Once

    type Singleton struct{}
    
    var (
        instancev3 *Singleton
        once       sync.Once
    )
    
    func GetInstancev3() *Singleton {
        once.Do(func() {
            instancev3 = &Singleton{}
        })
        return instancev3
    }
    

atomic 方案比读写锁的方案高效,atomic 本质上是一把乐观锁,而加锁读的开销要远大于乐观读。

sync.Once 实现:

func (o *Once) Do(f func()) {
   // Note: Here is an incorrect implementation of Do:
   //
   // if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
   //    f()
   // }
   //
   // Do guarantees that when it returns, f has finished.
   // This implementation would not implement that guarantee:
   // given two simultaneous calls, the winner of the cas would
   // call f, and the second would return immediately, without
   // waiting for the first's call to f to complete.
   // This is why the slow path falls back to a mutex, and why
   // the atomic.StoreUint32 must be delayed until after f returns.

   if atomic.LoadUint32(&o.done) == 0 {
      // Outlined slow-path to allow inlining of the fast-path.
      o.doSlow(f)
   }
}

func (o *Once) doSlow(f func()) {
   o.m.Lock()
   defer o.m.Unlock()
   if o.done == 0 {
      defer atomic.StoreUint32(&o.done, 1)
      f()
   }
}

sync.Once 相较于直接使用 atomic 拥有更高的性能,是因为利用了编译器优化,编译器会将规模比较小的代码做内联(inlining)处理,省去了函数调用的开销。之所有将 doSlow 单独封装成一个独立函数,是因为编译器不会将包含 defer 语句的函数做内联优化,所以 doSlow 单独封装后,Do 函数就会被内联优化,因为 doSlow 只会被执行一次,后续都是调用 Do 就返回,所以 Do 的内联优化了函数调用的开销。

中间件模式(Middleware)
#

中间件模式本质上是装饰模式(Decorator)在 Go 里的实现,他利用了 Go 函数式编程的思想。他可以动态添加一些横向的功能,类似于面向对象里的 AOP

一般多用在 http 等框架上,如 gin、hertz

package main

import (
   "log"
   "net/http"
)

// Handler defination
type Handler func(http.ResponseWriter, *http.Request)

// Middleware defination
type Middleware func(Handler) Handler

// Chain multi middleware to one
func Chain(mws ...Middleware) Middleware {
    return func(handler Handler) Handler {
        for _, mw := range mws {
            handler = mw(handler)
        }
        return handler
    }
}

// Logging middleware
func Logging(handler Handler) Handler {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Printf("[access] %s", r.URL.String())
        handler(w, r)
    }
}

func Hello(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("hello"))
}
func Echo(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("echo"))
}

func main() {
    http.HandleFunc("/hello", Logging(Hello))
    http.HandleFunc("/echo", Logging(Echo))

    log.Panic(http.ListenAndServe(":8080", nil))
}

选项模式(Option)
#

选项模式属于创建型模式的一种,适用于复杂对象的初始化,当对象初始化时大部分成员变量都使用默认值,只想对某一些成员赋值时,就可以使用选项模式

package main

type Config struct {
    ConnectTimeout int
    ReadTimeout    int
    MaxRetry       int
}

type Option func(*Config)

func WithConnectTimeout(timeout int) Option {
    return func(config *Config) {
        config.ConnectTimeout = timeout
    }
}

func WithReadTimeoutt(timeout int) Option {
    return func(config *Config) {
        config.ReadTimeout = timeout
    }
}

func WithMaxRetry(maxRetry int) Option {
    return func(config *Config) {
        config.MaxRetry = maxRetry
    }
}

type Client struct {
    c *Config
}

func NewClient(opts ...Option) *Client {
    config := &Config{
        ConnectTimeout: 1000,
        ReadTimeout:    2000,
        MaxRetry:       3,
    }
    for _, opt := range opts {
        opt(config)
    }
    client := &Client{
        c: config,
    }
    return client
}

func main() {
    _ = NewClient(
        WithConnectTimeout(500),
        WithMaxRetry(5),
    )
}

扇入模式(Fan-in)
#

将多个 channel 合并为一个 channel 称之为扇入,与之相对应的还有扇出,不过扇出的应用比较少。

使用场景如 一个函数的入参是一个 channel,但是需要把多个 channel 的数据抽象成一个 channel。

func Merge(channels ...<-chan int) <-chan int {
    var wg sync.WaitGroup

    out := make(chan int)

    // Start an send goroutine for each input channel in channels. send
    // copies values from c to out until c is closed, then calls wg.Done.
    send := func(c <-chan int) {
        for n := range c {
            out <- n
        }
        wg.Done()
    }

    wg.Add(len(channels))
    for _, c := range channels {
        go send(c)
    }

    // Start a goroutine to close out once all the send goroutines are
    // done.  This must start after the wg.Add call.
    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}

函数转接口对象(Function to interface object)
#

实现函数向接口对象的转换,这样使用者用起来就会非常灵活,在 net.http 包也用了这个模式(HandlerFunc),下面这个例子是把 Handle 函数转换成 Executor 接口的对象

type Executor interface {
    Exec(string) (string, error)
}

type ExecutorFunc func(string) (string, error)

// Function to interface object
func (f ExecutorFunc) Exec(s string) (string, error) {
    return f(s)
}

func NeedExecutor(executor Executor) {
    fmt.Println(executor.Exec("abc"))
}
func Handle(s string) (string, error) {
    return s + "handled", nil
}
func main() {
    NeedExecutor(ExecutorFunc(Handle))
}

plugin
#

Golang 是静态编译型语言,在编译时就将所有引用的包(库)全部加载打包到最终的可执行程序(或库文件)中,因此并不能在运行时动态加载其他共享库。Go Plugin 提供了这样一种方式,能够让你在运行时动态加载外部功能。

通过 go plugin,可以根据需要随时替换其中某些部件而不用修改主程序;动态加载外部的功能模块;Plugin 可以和主程序独立建设,随时替换 plugin

plugin 编写

package main

import (
   "context"
   "fmt"
   "sync/atomic"
   "time"
)

var idCounter uint64

func Echo(ctx context.Context, message string) {
    atomic.AddUint64(&idCounter, 1)
    fmt.Printf("this is plugin1: cnt:%d msg=%s\n", idCounter , message)
}

编译 plugin:go build -buildmode=plugin -o plugin.so。plugin 必须是在 main 包下,即 package mian,编译 plugin 的时候,必须使用 --buildmode=plugin 参数

主函数

package main

import (
    "context"
    "os"
    "plugin"
    "time"
)

func main() {
    successDemo()
}

func successDemo() {
    //加载plugin1
    echoPlugin, err := plugin.Open("/xxxxx/go_plugin_demo/plugins/plugin1/plugin.so")
    if err != nil {
        os.Exit(0)
    }

    go func() {
        time.Sleep(5* time.Second)
        // 加载plugin2
        echoPlugin, err = plugin.Open("/xxxxx/go_plugin_demo/plugins/plugin2/plugin.so")
        if err != nil {
            os.Exit(0)
        }
    }()

    for {
        echoSymbol, _ := echoPlugin.Lookup("Echo")
        ctx := context.Background()
        echoSymbol.(func(ctx context.Context, message string))(ctx, "hello")
        time.Sleep(time.Second)
    }
}

编译主函数:go build -o main main.go Lookup 只能查找可导出的成员(方法,变量等),并且返回参数为指针,需要转换成对应的类型才能使用。Lookup 不是并发安全的(底层是 Map),并发时候需要对其进行加锁

缺点

  • plugin 只有 OpenLookup 两个 API, 没有 close, 导致一个插件无法被关闭;
  • 需要 golang 的版本一致。golang的小版本,平台(x64,x32,darwin),如果不满足,将会见到以下的报错:plugin.Open("**"): plugin was built with a different version of package internal/cpu
  • plugin 与 main 代码的编译参数需要一致。如 debug 的时候,需要要加上 -gcflags='all=-N -l',但是我们的程序在 bootstrap 的时候主程序是不会带上这个参数的。(gcflags 的含义是:包名=参数列表,-l:disable inlining,-N:disable optimizations)
  • 依赖版本要一致,否则报错 plugin was built with a different version of package xxxxx

Related

c++ 性能
·2 mins
浅谈网络传输安全
·9 mins
浅谈认证、授权、凭证
·16 mins
ceph
·6 mins
c++ 无锁队列
·3 mins
Cgroup
·6 mins
Cgroup Cgroup