七叶笔记 » golang编程 » Go单例实现方案

Go单例实现方案

单例模式大家都比较了解,定义如下:一个类只允许创建唯一一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。

单例模式虽然理解起来比较简单,但是真正实现的时候有很多细节需要考虑,一般考虑点有如下几个:

  1. 构造函数需要是 private 访问权限的,这样才能避免外部通过 new 创建实例
  2. 考虑对象创建时的线程安全问题
  3. 考虑是否支持延迟加载
  4. 考虑 getInstance() 性能是否高(是否加锁)

代码

语法不同,对于这几点关注程度也不同。对于Go语言,在这里提供一种写法,使用sync.Once.Do。该函数的作用是只执行一次。

所以我们可以这么写:

 type Single struct {
}
var (
   once sync.Once
   single     *Single
)
func GetSingleInstance() *Single {
   once.Do(func() {
      single = &Single{}
   })
   return single
}
  

无论多少请求,只会有一个Single的实例。

测试

现在我们思考一个场景,如果突然有100个请求同时请求GetSingleInstance接口,这些请求是会等Do执行完还是无视Do直接return single呢?

理论上是需要等Do执行完的,否则返回的single为空,会导致严重错误。虽然是这么想,不过还是做个测试吧。

 package main
import (
   "fmt"
   "strconv"
   "sync"
   "time"
)
func main() {
   var once sync.Once
   onceBody := func() {
      fmt.Println("Only once start")
      time.Sleep(time.Second * 5)
      fmt.Println("Only once end")
   }
   for i := 0; i < 5; i++ {
      j := i
      go func(int) {
         fmt.Println(j)
         once.Do(onceBody)
         fmt.Println("lll" + strconv.Itoa(j))
      }(j)
   }
   fmt.Println("finished")
   time.Sleep(time.Second * 10)
}
  

测试方案很简单,启动5个goroutine,同时调用Do,onceBody设置sleep 5秒,只要检查输出,就能判断是否会阻塞。

执行结果如下:

➜ myproject go run main.go

finished 0

Only once start

2 4 3 1

Only once end

lll2 lll4 lll0 lll1 lll3

可以看出,只有Do执行完毕后,所有goroutine才会输出,证明Do都会被调用,但只有一个会真正执行,在真正的执行完前,其它goroutine会被阻塞。

其实这里有一个隐藏风险,如果Do执行的函数很耗时,会导致大量goroutine累积,编程的时候需要考虑到这一点。

具体实现

Do这个功能是如何实现的呢?让我们看一下源码:

 func (o *Once) Do(f func()) {
   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()
}
}
  

多个协程查看done值为0,进入doSlow,只有一个协程会获得锁,其它协程别阻塞。

其实用到的都是比较常规的技术,主要是互斥锁、信号量、defer,但是设计上还是很巧妙的。这也是Go的一个优势,解决冲突使用锁,快速、安全、方便,但是需要考虑好性能问题。

最后

大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)

我的个人博客为:

往期文章回顾:

技术

  1. Go设计模式(4)-代码编写
  2. Go设计模式(3)-设计原则
  3. Go设计模式(2)-面向对象分析与设计
  4. 支付接入常规问题
  5. HTTP2.0基础教程
  6. Go设计模式(1)-语法
  7. MySQL开发规范
  8. HTTPS配置实战
  9. Go通道实现原理
  10. Go定时器实现原理
  11. HTTPS连接过程
  12. 限流实现2
  13. 秒杀系统
  14. 分布式系统与一致性协议
  15. 微服务之服务框架和注册中心
  16. Beego框架使用
  17. 浅谈微服务
  18. TCP性能优化
  19. 限流实现1
  20. Redis实现分布式锁
  21. Golang源码BUG追查
  22. 事务原子性、一致性、持久性的实现原理
  23. CDN请求过程详解
  24. 常用缓存技巧
  25. 如何高效对接第三方支付
  26. Gin框架简洁版
  27. InnoDB锁与事务简析
  28. 算法总结

读书笔记

  1. 原则
  2. 资治通鉴
  3. 敏捷革命
  4. 如何锻炼自己的记忆力
  5. 简单的逻辑学-读后感
  6. 热风-读后感
  7. 论语-读后感
  8. 孙子兵法-读后感

思考

  1. 反对自由主义
  2. 实践论
  3. 评价自己的标准
  4. 服务端团队假期值班方案
  5. 项目流程管理
  6. 对项目管理的一些看法
  7. 对产品经理的一些思考
  8. 关于程序员职业发展的思考
  9. 关于代码review的思考
  10. Markdown编辑器推荐-typora

相关文章