您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > Go语言

Go 和 Java 对比学习:单例模式

时间:2020-09-16 09:56:34  来源:  作者:

JAVA 是较典型的面向对象语言。如果说 C++ 是设计模式的发源地(GoF 的书使用 C++ 描述的),那么 Java 将设计模式发扬光大。设计模式,很多人可能工作中没有用到,因为大部分人停留在写面条式的业务代码,从头撸到尾,没有设计可言。但实际上,只要你用心思考,这样的场景下也是很有可能用上设计模式的。特别是,当系统复杂时,设计模式的作用会很明显。

虽然 Go 语言并非完全的面向对象语言,只提供了部分面向对象的特性,但一些设计模式还是可以使用的。这个系列尝试讲解在 Go 中使用设计模式,同时给出 Java 对应的版本,进行对比学习。另外,我们的设计模式不会局限在 GoF 的 23 中设计模式之中。

在开始设计模式之前,有必要提一下面向对象的 SOLID 5 大设计原则:

名称缩写含义The Single Responsibility Principle(单一职责)S对象应该具有单一的职责。这也是 Unix 的设计哲学The Open/Closed Principle(开/闭原则)O对扩展开发,对修改关闭The Liskov Substitution Principle(里氏替换)L对象应该可以在不破坏系统的情况下被子对象替换The Interface Segregation Principle(接口隔离)I不应强迫任何客户端依赖其不使用的方法The Dependency Inversion Principle(依赖倒转)D高级模块不应依赖于低级实现

遵循这样的设计原则,你的系统会更好维护。

除了 SOLID 5 大设计原则,一些书上可能还会提到下面的设计原则:

  • 合成/聚合复用原则(Composite/Aggregate Reuse Principle):尽量使用合成/聚合,而不要使用继承。这也是 Go 语言设计遵循的,基于此,Go 中没有继承。
  • 迪米特法则(LoD),又叫 最少知识原则:一个对象应当对其他对象有尽可能少的了解;一个软件实体应当与尽可能少的其他实体发生相互作用。

在你日常的工作中,可以运用以上原则审视你的设计,改进你的设计。

今天先看第一个设计模式。

1、单例模式简介

面向对象中的单例模式是一个常见、简单的模式。

英文名称:Singleton Pattern,该模式规定一个类只允许有一个实例,而且自行实例化并向整个系统提供这个实例。因此单例模式的要点有:1)只有一个实例;2)必须自行创建;3)必须自行向整个系统提供这个实例。

单例模式主要避免一个全局使用的类频繁地创建与销毁。当你想控制实例的数量,或有时候不允许存在多实例时,单例模式就派上用场了。

先看 Java 中的单例模式。

Go 和 Java 对比学习:单例模式

 

通过该类图我们可以看出,实现一个单例模式有如下要求:

  • 私有、静态的类实例变量;
  • 构造函数私有化;
  • 静态工厂方法,返回此类的唯一实例;

根据实例化的时机,单例模式一般分成饿汉式和懒汉式。

  • 饿汉式:在定义 instance 时直接实例化,private static Singleton instance = new Singleton();
  • 懒汉式:在 getInstance 方法中进行实例化;

那两者有什么区别或优缺点?饿汉式单例类在自己被加载时就将自己实例化。即便加载器是静态的,饿汉式单例类被加载时仍会将自己实例化。单从资源利用率角度讲,这个比懒汉式单例类稍差些。从速度和反应时间角度讲,则比懒汉式单例类稍好些。然而,懒汉式单例类在实例化时,必须处理好在多个线程同时首次引用此类时的访问限制问题,特别是当单例类作为资源控制器在实例化时必须涉及资源初始化,而资源初始化很有可能耗费时间。这意味着出现多线程同时首次引用此类的几率变得较大。

2、单例模式的 Java 实现

结合上面的讲解,以一个计数器为例,我们看看 Java 中饿汉式的实现:

public class Singleton {
  private static final Singleton instance = new Singleton();
  private int count = 0;
  private Singleton() {}
  public static Singleton getInstance() {
    return instance;
  }  public int Add() int {
    this.count++;
    return this.count;
  }}

代码很简单,不过多解释。直接看懒汉式的实现:

public class Singleton {
  private static Singleton instance = null;
  private int count = 0;
  private Singleton() {}
  public static synchronized Singleton getInstance() {
    if (instance == null) {
      instance = new Singleton();
    }    return instance;
  }  public int Add() int {
    this.count++;
    return this.count;
  }}

主要区别在于 getInstance 的实现,要注意 synchronized ,避免多线程时出现问题。

3、单例模式的 Go 实现

在 Go 语言中如何实现单例模式,类比 Java 代码实现。

// 饿汉式单例模式
package singleton
type singleton struct {  count int
}var Instance = new(singleton)func (s *singleton) Add() int {
  s.count++  return s.count
}

前面说了,Go 只支持部分面向对象的特性,因此看起来有点不太一样:

  • 类(结构体 singleton)本身非公开(小写字母开头,非导出);
  • 没有提供导出的 GetInstance 工厂方法(Go 没有静态方法),而是直接提供包级导出变量 Instance;

这样使用:

c := singleton.Instance.Add()

看看懒汉式单例模式在 Go 中如何实现:

// 懒汉式单例模式
package singleton
import ( "sync"
)type singleton struct {
  count int}var (  instance *singleton  mutex sync.Mutex)func New() *singleton {  mutex.Lock()  if instance == nil {
    instance = new(singleton)  }  mutex.Unlock()    return instance
}func (s *singleton) Add() int {  s.count++  return s.count
}

代码多了不少:

  • 包级变量变成非导出(instance),注意这里类型应该用指针,因为结构体的默认值不是 nil;
  • 提供了工厂方法,按照 Go 的惯例,我们命名为 New();
  • 多 goroutine 保护,对应 Java 的 synchronized,Go 使用 sync.Mutex;

关于懒汉式有一个“双重检查”,这是 C 语言的一种代码模式。

在上面 New() 函数中,同步化(锁保护)实际上只在 instance 变量第一次被赋值之前才有用。在 instance 变量有了值之后,同步化实际上变成了一个不必要的瓶颈。如果能够有一个方法去掉这个小小的额外开销,不是更加完美吗?因此出现了“双重检查”。看看 Go 如何实现“双重检查”,只看 New() 代码:

func New() *singleton {
  if instance == nil { // 第一次检查(①)
    // 这里可能有多于一个 goroutine 同时达到(②)
    mutex.Lock()
    // 这里每个时刻只会有一个 goroutine(③)
    if instance == nil { // 第二次检查(④)
      instance = new(singleton)
    }
    mutex.Unlock()
  }
  
  return instance
}

有读者可能看不懂上面代码的意思,这里详细解释下。假设 goroutine X 和 Y 作为第一批调用者同时或几乎同时调用 New 函数。

  1. 因为 goroutine X 和 Y 是第一批调用者,因此,当它们进入此函数时,instance 变量是 nil。因此 goroutine X 和 Y 会同时或几乎同时到达位置 ①;
  2. 假设 goroutine X 会先达到位置 ②,并进入 mutex.Lock() 达到位置 ③。这时,由于 mutex.Lock 的同步限制,goroutine Y 无法到达位置 ③,而只能在位置 ② 等候;
  3. goroutine X 执行 instance = new(singleton) 语句,使得 instance 变量得到一个值,即对 singleton 实例的引用。此时,goroutine Y 只能继续在位置 ② 等候;
  4. goroutine X 释放锁,返回 instance,退出 New 函数;
  5. goroutine Y 进入 mutex.Lock(),到达位置 ③,进而到达位置 ④。由于 instance 变量已经不是 nil,因此 goroutine Y 释放锁,返回 instance 所引用的 singleton 实例(也就是 goroutine X 锁创建的 singleton 实例),退出 New 函数;

到这里,goroutine X 和 Y 得到了同一个 singleton 实例。可见上面的 New 函数中,锁仅用来避免多个 goroutine 同时实例化 singleton。

相比前面的版本,双重检查版本,只要 instance 实例化后,锁永远不会执行了,而前面版本每次调用 New 获取实例都需要执行锁。性能很显然,我们可以基准测试来验证:(双重检查版本 New 重命名为 New2)

package singleton_test
import ( "testing"
 "github.com/polaris1119/go-demo/singleton"
)func BenchmarkNew(b *testing.B) {
 for i := 0; i < b.N; i++ {
  singleton.New() }}func BenchmarkNew2(b *testing.B) {
 for i := 0; i < b.N; i++ {
  singleton.New2() }}

因为是单例,所以两个基准测试需要分别执行。

New1 的结果:

$ go test -benchmem -bench ^BenchmarkNew$ github.com/polaris1119/go-demo/singleton
goos: darwin
goarch: amd64
pkg: github.com/polaris1119/go-demo/singleton
BenchmarkNew-8    80470467         14.0 ns/op        0 B/op        0 allocs/op
PASS
ok   github.com/polaris1119/go-demo/singleton 1.151s

New2 的结果:

$ go test -benchmem -bench ^BenchmarkNew2$ github.com/polaris1119/go-demo/singleton
goos: darwin
goarch: amd64
pkg: github.com/polaris1119/go-demo/singleton
BenchmarkNew2-8    658810392          1.80 ns/op        0 B/op        0 allocs/op
PASS
ok   github.com/polaris1119/go-demo/singleton 1.380s

New2 快十几倍。

细心得读者会发现,在 Go 中,饿汉式还有一种更好的实现方式,那就是使用 sync.Once,这是 Go 实现懒汉式更标准的做法。核心代码如下(New3):

var once sync.Once
func New3() *singleton {    once.Do(func() {
        instance = new(singleton)
    })    return instance
}

通过基准测试,它的性能和 New2 差不多。

此外,无论是 Java 还是 Go,都有一些其他“黑魔法”,比如 Go 语言中,利用 init 函数来初始化唯一的单例。不过一般都不太建议,还是常规方式来。

Go 语言单例模式,一般推荐优先考虑使用饿汉式。但如果初始化比较耗时,懒汉式延迟初始化是更好的选择。

4、使用场景

在 Go 语言中,如下两个场景比较适合使用单例模式:

  • 数据库实例。只想创建一个 DB 对象实例,该实例在整个应用程序中使用。
  • 日志实例。同样,只创建一个 Logger 的实例,并且在整个应用程序中使用它。

参考资料

  • https://github.com/dwmkerr/hacker-laws


Tags:Go语言   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
zip 是一种常见的归档格式,本文讲解 Go 如何操作 zip。首先看看 zip 文件是如何工作的。以一个小文件为例:(类 Unix 系统下)$ cat hello.textHello!执行 zip 命令进行归档:$ zip...【详细内容】
2021-12-17  Tags: Go语言  点击:(12)  评论:(0)  加入收藏
统一规范篇合理规划目录本篇主要描述了公司内部同事都必须遵守的一些开发规矩,如统一开发空间,既使用统一的开发工具来保证代码最后的格式的统一,开发中对文件和代码长度的控制...【详细内容】
2021-05-18  Tags: Go语言  点击:(232)  评论:(0)  加入收藏
闭包概述 闭包不是Go语言独有的概念,在很多编程语言中都有闭包 闭包就是解决局部变量不能被外部访问的一种解决方案 是把函数当作返回值的一种应用 代码演示总体思想:在函数...【详细内容】
2021-05-14  Tags: Go语言  点击:(223)  评论:(0)  加入收藏
一时想不开,想了解一下Go语言,于是安装了并体验了一下。下载1. 进入golang.google.cn 点击Download Go 2.选择对应的操作系统,点击后开始下载。 安装1. windows下执行傻瓜式安...【详细内容】
2021-05-12  Tags: Go语言  点击:(236)  评论:(0)  加入收藏
再次整理了一下这个日志收集系统的框,如下图 这次要实现的代码的整体逻辑为: 完整代码地址为: https://github.com/pythonsite/logagentetcd介绍高可用的分布式key-value存储,...【详细内容】
2021-01-14  Tags: Go语言  点击:(135)  评论:(0)  加入收藏
生命不止,继续 Go go go !!!之前关于golang操作数据库的博客:Go实战&ndash;go语言操作MySQL数据库(go-sql-driver/mysql)Go实战&ndash;go语言操作sqlite数据库(The way to go)...【详细内容】
2021-01-05  Tags: Go语言  点击:(196)  评论:(0)  加入收藏
欢迎访问我的GitHubhttps://github.com/zq2599/blog_demos内容:所有原创文章分类和汇总,及配套源码,涉及Java、Docker、Kubernetes、DevOPS等;关于《gRPC学习》系列《gRPC学习》...【详细内容】
2020-12-14  Tags: Go语言  点击:(124)  评论:(0)  加入收藏
概述Go语言作为一门开源的编程语言,以简洁、快速、安全著称。尤其在高性能的分布式服务器领域得到广泛应用。技多不压身,在学习过程中记录下来,以备后续参考,希望对有同样需求的...【详细内容】
2020-11-12  Tags: Go语言  点击:(65)  评论:(0)  加入收藏
go-fly基于GO语言实现的web客服即时通讯与客服管理系统。非常适合给自己的网站增加在线客服功能,代码简单也适合学习。Github地址:https://github.com/taoshihan1991/go-fly1....【详细内容】
2020-09-25  Tags: Go语言  点击:(154)  评论:(0)  加入收藏
Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee Fℹ️ 这篇文章基于 Go 1.13。在内存从分配到回收的生命周期中,内存...【详细内容】
2020-09-24  Tags: Go语言  点击:(65)  评论:(0)  加入收藏
▌简易百科推荐
zip 是一种常见的归档格式,本文讲解 Go 如何操作 zip。首先看看 zip 文件是如何工作的。以一个小文件为例:(类 Unix 系统下)$ cat hello.textHello!执行 zip 命令进行归档:$ zip...【详细内容】
2021-12-17  Go语言中文网    Tags:Go语言   点击:(12)  评论:(0)  加入收藏
大家好,我是 polarisxu。前段时间,Russ Cox 明确了泛型相关的事情,原计划在标准库中加入泛型相关的包,改放到 golang.org/x/exp 下。目前,Go 泛型的主要设计者 ianlancetaylor 完...【详细内容】
2021-11-30  Go语言中文网    Tags:slices 包   点击:(24)  评论:(0)  加入收藏
前言最近因为项目需要写了一段时间的 Go ,相对于 Java 来说语法简单同时又有着一些 Python 之类的语法糖,让人大呼”真香“。 但现阶段相对来说还是 Python 写的多一些,偶尔还...【详细内容】
2021-11-25  crossoverJie    Tags:Go   点击:(29)  评论:(0)  加入收藏
go-micro是基于 Go 语言用于开发的微服务的 RPC 框架,主要功能如下:服务发现,负载均衡 ,消息编码,请求/响应,Async Messaging,可插拔接口,最后这个功能牛p安装步骤安装proto...【详细内容】
2021-09-06    石老师小跟班  Tags:go-micro   点击:(196)  评论:(0)  加入收藏
GoLand 2021.2 EAP 5 现已发布。用户可以从工具箱应用程序中获得 EAP 构建,也可以从官方网站手动下载。并且从此 EAP 开始,只有拥有有效的 JetBrains 帐户才能加入该计划。手...【详细内容】
2021-06-29  IT实战联盟  今日头条  Tags:GoLand   点击:(185)  评论:(0)  加入收藏
作者:HDT3213今天给大家带来的开源项目是 Godis:一个用 Go 语言实现的 Redis 服务器。支持: 5 种数据结构(string、list、hash、set、sortedset) 自动过期(TTL) 发布订阅、地理位...【详细内容】
2021-06-18  HelloGitHub  今日头条  Tags:Go   点击:(125)  评论:(0)  加入收藏
统一规范篇合理规划目录本篇主要描述了公司内部同事都必须遵守的一些开发规矩,如统一开发空间,既使用统一的开发工具来保证代码最后的格式的统一,开发中对文件和代码长度的控制...【详细内容】
2021-05-18  1024课堂    Tags:Go语言   点击:(232)  评论:(0)  加入收藏
闭包概述 闭包不是Go语言独有的概念,在很多编程语言中都有闭包 闭包就是解决局部变量不能被外部访问的一种解决方案 是把函数当作返回值的一种应用 代码演示总体思想:在函数...【详细内容】
2021-05-14  HelloGo  今日头条  Tags:Go语言   点击:(223)  评论:(0)  加入收藏
一时想不开,想了解一下Go语言,于是安装了并体验了一下。下载1. 进入golang.google.cn 点击Download Go 2.选择对应的操作系统,点击后开始下载。 安装1. windows下执行傻瓜式安...【详细内容】
2021-05-12  程序员fearlazy  fearlazy  Tags:Go语言   点击:(236)  评论:(0)  加入收藏
1.简介channel是Go语言的一大特性,基于channel有很多值得探讨的问题,如 channel为什么是并发安全的? 同步通道和异步通道有啥区别? 通道为何会阻塞协程? 使用通道导致阻塞的协程...【详细内容】
2021-05-10  程序员麻辣烫  今日头条  Tags:Go通道   点击:(272)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条