序言
每种编程语言都有其独特的语法,而特定的语法也反映了该编程语言被创建之初的意图,即为了解决某种旧语言的一些痛点。比如说Golang的发明,Google公司最开始还是使用C++来做工程开发,但C++有一些明显的痛点,比如说编译速度很慢,大型项目的一次构建长达2小时以上,同时还存在内存泄漏的风险,其次对并发的支持也不是很好,那么Google的几个大佬在C++语言的基础上,进行了一些修正,具体可见于Rob Pike关于go设计思路的那封邮件。
本文不想直接的罗列出Golang的一堆高级特性,然后依次讲解,而想换一种思路,结合大牛们基于Go语言实现的设计模式的代码,让大家直观的感受Go语言高级特性实战的用法和Golang的设计哲学,less is more!
简单工厂模式
语法关键词:接收者函数,接口类型interface,空结构体struct{}
那么开始,我们实现一个简单工厂模式,它的格式一般是通过入参枚举的不同,返回特定的类,然后这些类都是有一个同名函数的。比如说苹果,橘子,桃子三个产品,然后要有一个show函数告诉我一斤多少钱啊,如果要是C++写的话,那肯定要定义一个基类和一个纯虚函数,子类重写该函数即可。
但是Go是没有类的概念,当然也就没有类构造函数和类方法,对于Go来说只有普通的函数,这省去了复杂的继承和派生的逻辑,Go设计认为用正交组合的方式去组织代码要更加简单解耦。Go为了实现类似类方法和多态的感觉,引入了接收者函数和接口类型interface,接收者函数比C++那种普通函数声明多了一处不同的就是函数名前面多了一个叫接收器的东西,它定义这些函数将在其上运行的对象的一种约定格式,即类似Java接口的感觉:
那么,我们看Go语言具体如何实现简单工厂模式:
package simplefactory
import (
"fmt"
)
// interface代表接口类型,Fruit都有一个展示价格的函数
type Fruit interface {
HowMuch()
}
func NewFruit(t int) Fruit {
if t == 1 {
return &Apple{}
} else if t == 2 {
return &Banana{}
}
return nil
}
type Apple struct{}
func (*Apple) HowMuch() {
fmt.Printf("Hi, Apple 一块钱 一斤\n")
}
type Banana struct{}
func (*Banana) HowMuch() {
fmt.Printf("Hi, Banana 九毛钱 一斤\n")
}
单例模式
语法关键词:包内函数和变量的私有化,sync.Once,协程,chan,等待组
单例模式的实现老生常谈啦,一面的时候总考,我一波构造函数私有化+互斥锁+double check全给他防出去了,那么Go语言实现这个模式会是怎样的呢?首先函数私有的问题,Go语言不像C++有 public、protected、private 等访问控制修饰符,其次Go舍弃了C++中include头文件的习惯,而是引入类似于python的import包,因此,Go是通过字母大小写以及下划线开头来控制可见性的,大写字母开头表示能被其它包访问或调用(相当于 public),非大写开头表示只能在包内使用(相当于 private):
package test_private_and_public
const (
PubicVar = `大写变量开头,这是一个公开的常量`
privateVar = `小写变量开头,这是一个私有的常量`
_privateVar = `下划线开头, 这是一个私有的常量`
)
func PubicFuc() {
println(`这是一个公开的函数`)
}
func privateFuc() {
println(`这是一个私有的函数`)
}
func _privateFuc() {
println(`这也是一个私有的函数`)
}
这样我们就可以把单例的结构体搞成小写的,这样外面引用的人只能使用我们的公开的构造函数去创建对象,而不会直接自己就可以new啦。
其次,只执行一次是怎么做到呢?答案是用到了sync.Once这个特性。sync.Once 是 Go 标准库提供的使函数只执行一次的实现,可以在代码的任意位置初始化和调用,因此可以延迟到使用时再执行,并发场景下是线程安全的。
到了这步,单例模式的Go实现呼之欲出了,但是我们如何测试并发的场景呢,即同时要有多个线程让他都调用构造函数得到一个金斧子,然后等他们都得到一个金斧子单例的时候(这要考虑同步过程),再验证这些金斧子都是同一把呢?
并发的解决方法便是利用go协程,它是在应用层模拟的线程,他避免了上下文切换的额外耗费,兼顾了多线程的优点,这也是应了Golang的设计之初的目的就是为了解决C++并发的复杂性。go协程开启通过关键字 go 启用多个协程,然后在不同的协程中完成不同的子任务,这些用户在代码中创建和维护的协程本质上是用户级线程,用户代码的执行最后还是要落到系统级的线程中的,其内部是维护了一组数据结构, n 个线程和一个待执行队列。协程的切换是golang利用系统级异步 io函数的封装,这些封装的函数提供给应用程序使用,当这些异步函数返回 busy 或 bloking 时,golang 利用这个时机将现有的执行序列压栈,让线程去拉另外一个协程的代码来执行,这达到的协程切换的目的。
由于golang是从编译器和语言基础库多个层面对协程做了实现,所以,golang的协程是目前各类有协程概念的语言中实现的最完整和成熟的。十万个协程同时运行也毫无压力。
好啦,现在并发的条件有了,那么我们需要有协程同步的机制,大家得凝成一个势力才可以发挥更大的能量,这里利用的是特性是chan和等待组,等待组和C++中的信号量很像,通过PV操作达到同步的机制。chan是Go中专属的叫通道,它是一个先进先出的队列,入队和出队会阻塞的作用,他的声明分为以下四种:
// 不带缓冲的通道,它里面可以放字符串类型,进和出都会阻塞。
ch1 := make(chan string)
// 队列长度为5个的通道,它里面可以放字符串类型,如果通道内元素达到队列长度时,再进就会阻塞。
ch2 := make(chan string, 5)
// 只读通道
ch3 := make(<-chan string)
// 只写通道
ch4 := make(chan<- string)
一个chan的小李子:
package main
import (
"fmt"
)
func main() {
// 构建一个通道
ch := make(chan int)
// 开启一个并发匿名函数
go func() {
fmt.Println("start goroutine")
// 通过通道通知main的goroutine,接受之前我阻塞在这里
ch <- 0
fmt.Println("exit goroutine")
}()
fmt.Println("wait goroutine")
// 读取通道的值,如果没有就阻塞
<-ch
fmt.Println("all done")
}
//运行结果
PS D:\code\golang-design-pattern\03_singleton> go run .\2n.go
wait goroutine
start goroutine
exit goroutine
all done
准备工作都做好了,那么让我们开始写一个单例模式吧。
//实现部分
package singleton
import (
"fmt"
"sync"
)
// 包私有,该结构体不能直接使用
type goldaxe struct{}
func (axe goldaxe) TellTruth() {
fmt.Printf("小伙汁你太诚实啦,金斧子是你的了\n")
}
// 全局变量
var (
axe *goldaxe
once sync.Once
)
// 由于单例类型不能在包外直接使用,用一个接口类型带出去
type GoldAxe interface {
TellTruth()
}
// 用于获取单例模式对象,大家都是一样的斧子
func GetGoldAxe() GoldAxe {
once.Do(func() {
axe = &goldaxe{}
})
return axe
}
//测试部分
package singleton
import (
"sync"
"testing"
)
const axeCounts = 100
func Test1(t *testing.T) {
ins1 := GetGoldAxe()
ins1.TellTruth()
ins2 := GetGoldAxe()
if ins1 != ins2 {
t.Fatal("instance is not equal")
}
}
func Test2(t *testing.T) {
start := make(chan struct{})
//信号量初始化
wg := sync.WaitGroup{}
//信号量搞个100个
wg.Add(axeCounts)
//金斧子数组,并进行了列表初始化
//这么写你紫定get了:var float_array = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
instances := [axeCounts]GoldAxe{}
for i := 0; i < axeCounts; i++ {
//开启100个协程
go func(index int) {
//获取channel的值,由于没有协程只能阻塞
<-start
instances[index] = GetGoldAxe()
wg.Done()
}(i)
}
//关闭channel,所有协程同时GetGoldAxe,达到了并发创建实例的情况
close(start)
//等待大家都获得自己金斧子后,才到下一步
wg.Wait()
for i := 1; i < axeCounts; i++ {
if instances[i] != instances[i-1] {
t.Fatal("instance is not equal")
}
}
}
Reference
Go sync.Once | Go 语言高性能编程 | 极客兔兔 (geektutu.com)
[系列] Go - chan 通道 - 新亮笔记 - 博客园 (cnblogs.com)
Go语言等待组(sync.WaitGroup) (biancheng.net)
Golang 之协程详解 - 星火燎原智勇 - 博客园 (cnblogs.com)
Go 语言并发编程系列(二)—— Go 协程实现原理和使用示例 - 腾讯云开发者社区-腾讯云 (tencent.com)
Go语言通道(chan)——goroutine之间通信的管道 (biancheng.net)