Java是典型的面向对象语言。如果说C++是设计模式的发源地(GoF书中使用C++来描述它们),那么Java将设计模式提升到了一个新的水平。设计模式,很多人在工作中可能不会用到,因为大部分人都停留在写面条一样的业务代码,根本谈不上设计。但实际上,只要你用心思考,在这样的场景下使用设计模式是很有可能的。特别是当系统比较复杂的时候,设计模式的作用就会很明显。Go语言虽然不是完全面向对象的语言,只提供了一些面向对象的特性,但是一些设计模式还是可以使用的。本系列试图讲解Go中设计模式的使用,同时给出相应版本的Java进行对比学习。另外,我们的设计模式并不局限于GoF的23种设计模式。在开始设计模式之前,有必要提一下面向对象的SOLID5大设计原则:首字母缩写意思是TheSingleResponsibilityPrinciple(单一职责)S个对象应该有单一的职责。这也是Unix的设计哲学开闭原则(Open/ClosedPrinciple)O用于扩展开发,封闭用于修改里氏替换原则(LiskovSubstitution)L对象应该能够被子对象替换而不破坏系统接口隔离原则(InterfaceSegregation)Ishouldnotforceanyclienttorelyonmethodsitdoesnotuse依赖倒置原则(DependencyInversion)D高层模块不应该依赖低层实现遵循这个设计原则和你的系统会得到更好的维护。除了SOLID的5大设计原则外,一些书籍可能还会提到以下设计原则:Composite/AggregateReusePrinciple(复合/聚合重用原则):尽量使用组合/聚合而不是继承。这也是Go语言设计所遵循的,基于此,Go中没有继承。迪米特法则(LoD),也称为最少知识原则:一个对象应该尽可能少地了解其他对象;软件实体应该与尽可能少的其他实体交互。在您的日常工作中,您可以使用以上原则来审查您的设计并改进您的设计。今天我们来看第一种设计模式。一、单例模式简介面向对象中的单例模式是一种常见的简单模式。英文名称:SingletonPattern,规定一个类只允许有一个实例,并实例化自己并将这个实例提供给整个系统。因此,单例模式的要点是:1)只有一个实例;2)必须自己创建;3)这个实例必须自己提供给整个系统。单例模式主要避免频繁创建和销毁一个全局使用的类。当你想控制实例的数量,或者有时不允许多个实例时,单例模式就派上用场了。先来看Java中的单例模式。通过这个类图,我们可以看出实现单例模式有如下需求:私有的、静态的类实例变量;建设者私有化;静态工厂方法,它返回这个类的唯一实例;模式一般分为饿和懒。饿了么风格:定义实例时直接实例化,privatestaticSingletoninstance=newSingleton();惰性风格:在getInstance方法中实例化;两者有什么区别或优缺点?饿类型单例类是在加载时实例化自身。即使加载器是静态的,饥饿的单例类在加载时仍然会实例化自己。单从资源利用的角度来看,这比惰性单例类略差。在速度和响应时间上,略优于惰性单例类。但是,在实例化惰性风格的单例类时,必须处理多个线程同时首次引用该类时的访问限制问题,尤其是单例类实例化为资源控制器时,资源初始化必须涉及,并且资源初始化很可能是耗时的。这意味着多个线程第一次同时引用这个类的可能性更大。2.单例模式的Java实现结合上面的讲解,以计数器为例,再看饿汉的Java实现:publicclassSingleton{privatestaticfinalSingletoninstance=newSingleton();privateintcount=0;privateSingleton(){}publicstaticSingletongetInstance(){returninstance;}publicintAdd()int{this.count++;returnthis.count;}}代码很简单,就不多解释了。直接看懒人的实现:publicclassSingleton{privatestaticSingletoninstance=null;privateintcount=0;privateSingleton(){}publicstaticsynchronizedSingletongetInstance(){if(instance==null){instance=newSingleton();}returninstance;}publicintAdd()int{this.count++;returnthis.count;}}主要区别在于getInstance的实现,注意synchronized避免多线程出现问题。3.Go单例模式的实现Go语言如何实现单例模式类似于Java代码实现。//饿汉单例模式包singletontypesingletonstruct{countint}varInstance=new(singleton)func(s*singleton)Add()int{s.count++returns.count}前面说了Go只支持一些面向对象的特性,所以它看起来有点不同:类(结构单例)本身是非公开的(以小写字母开头,不导出);不提供导出的GetInstance工厂方法(Go没有静态方法),而是直接提供包级别的导出变量Instance;使用这个:c:=singleton.Instance.Add()看看惰性单例模式是如何在Go中实现的://惰性单例模式packagesingletonimport("sync")typesingletonstruct{countint}var(instance*singletonmutexsync.Mutex)funcNew()*singleton{mutex.Lock()ifinstance==nil{instance=new(singleton)}mutex.Unlock()returninstance}func(s*singleton)Add()int{s.count++returns.count}的代码多了很多:包级变量变为非导出(实例),注意这里的类型应该是指针,因为结构体的默认值不是nil;提供了一个工厂方法,根据Go的约定,我们将其命名为New();多goroutine保护,对应Java的synchronized,Go使用sync.Mutex;惰性风格有个“双重检查”,是C语言中的一种代码模式。在上面的New()函数中,同步(锁保护)实际上只有在实例变量第一次被赋值时才有用。实例变量有了值之后,同步其实就成了一个不必要的瓶颈。如果有办法消除这一点额外开销,岂不是更完美?因此,“双重检查”。看Go如何实现“双重检查”,只看New()代码:funcNew()*singleton{ifinstance==nil{//第一次检查(①)//可能有多个goroutine同时到达(②)mutex.Lock()//这里每一时刻只会有一个goroutine(③)ifinstance==nil{//第二次检查(④)instance=new(singleton)}mutex.Unlock()}returninstance}有读者可能不理解上面代码的意思,这里做一个详细的解释。假设goroutineX和Y在与第一个调用者同时或几乎同时调用New函数。因为协程X和Y是最先调用的,所以当他们进入这个函数时,实例变量为nil。所以goroutineX和Y会同时或者几乎同时到达位置①;假设goroutineX先到达位置②,进入mutex.Lock()到达位置③。此时由于mutex.Lock的同步限制,goroutineY无法到达位置③,只能在位置②等待;goroutineX执行instance=new(singleton)语句,使实例变量得到一个值,即对单例实例的引用。此时goroutineY只能在位置②继续等待;goroutineX释放锁,返回实例,退出New函数;goroutineY进入mutex.Lock(),到达位置③,然后到达位置④。由于实例变量不为nil,goroutineY释放锁,返回instance引用的单例实例(即goroutineX锁创建的单例实例),退出New函数;在这里,goroutineX和Y得到相同的单例实例。可以看出,在上面的New函数中,锁只是用来防止多个goroutine同时实例化单例。相比之前的版本,doublecheck版本,只要实例化了实例,就永远不会执行锁,而之前的版本每次调用New获取实例都需要执行锁。性能显然,我们可以通过基准测试来验证:(仔细检查版本New更名为New2)packagesingleton_testimport("testing""github.com/polaris1119/go-demo/singleton")funcBenchmarkNew(b*testing.B){fori:=0;i
