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 大设计原则,一些书上可能还会提到下面的设计原则:
在你日常的工作中,可以运用以上原则审视你的设计,改进你的设计。
今天先看第一个设计模式。
面向对象中的单例模式是一个常见、简单的模式。
英文名称:Singleton Pattern,该模式规定一个类只允许有一个实例,而且自行实例化并向整个系统提供这个实例。因此单例模式的要点有:1)只有一个实例;2)必须自行创建;3)必须自行向整个系统提供这个实例。
单例模式主要避免一个全局使用的类频繁地创建与销毁。当你想控制实例的数量,或有时候不允许存在多实例时,单例模式就派上用场了。
先看 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 ,避免多线程时出现问题。
在 Go 语言中如何实现单例模式,类比 Java 代码实现。
// 饿汉式单例模式
package singleton
type singleton struct { count int
}var Instance = new(singleton)func (s *singleton) Add() int {
s.count++ return s.count
}
前面说了,Go 只支持部分面向对象的特性,因此看起来有点不太一样:
这样使用:
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
}
代码多了不少:
关于懒汉式有一个“双重检查”,这是 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 函数。
到这里,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 语言单例模式,一般推荐优先考虑使用饿汉式。但如果初始化比较耗时,懒汉式延迟初始化是更好的选择。
在 Go 语言中,如下两个场景比较适合使用单例模式: