单例模式 #
双重检查锁版本的单例模式:
// 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 只有
Open
与Lookup
两个 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