1.语言进阶
我们来看Go语言为什么性能这么好
并发VS并行
并发是指多线程在一个核的cpu上运行,通过时间切换来实现;而并行是指多线程在多个核的cpu上运行,通过多核直接实现多线程同时运行。
并行可以看成是并发的一种实现方式。
Go语言实现了一个调度并发的一个高效的模型,通过高效的调度使得多并发的性能优异。
1.1Goroutine
线程属于系统比较昂贵的一个资源,属于内核态。而协程——Goroutine属于轻量级的资源,属于用户态。
Golang可以实现在线程上跑多个协程,可以创建上万的左右的协程。
如何开启协程:
示例:快速打印hello goroutine:0到4(重要的是快速,就要用到协程)
package main
import(
"fmt"
"time"
)
func hello(i int){
fmt.Printf("Hello goroutine:%v\n",i)
}
func HelloGoroutine(){
for i:=0;i<5;i++{
go func(j int){//这里是什么意思呢,这里是一个匿名函数,go关键字后面跟的是一个函数,这个函数就是要开启的协程
//j是匿名函数要使用的参数
hello(j)
}(i)//将i拷贝给j,这样匿名函数就可以使用i了
//这段代码的原理是,开启了5个协程,每个协程都会调用hello函数,但是hello函数的参数不同
}
time.Sleep(time.Second)//这里是为了让主线程等待一秒钟,以便协程可以输出
//这里其实还有更优雅的方法,我们以后再说
//在golang中开启协程是很简单的,只需要在函数前面加上go关键字就可以了
}
在golang中我们用go关键字加在函数前面实现多协程开发
我们发现该输出为乱序,说明多个协程是并行打印的
1.2CSP
CSP即是Communicating Sequential Processes,即协程间的通信,Golang鼓励协程之间通过通信来共享内存以便更高效的利用空间,而不是共享内存实现通信
通信就涉及到一个比较重要的点——通道
使用通信实现内存共享的原理是通道将协程之间进行连接,类似于队列,从而保证数据收发的顺序
通道就是让一个goroutine发送特定的值到另一个goroutine的机制
而使用内存实现通信则需要用到互斥锁,go也保留着这个机制
后者会产生性能的问题,go推荐前一种
Channel(通道)的实现:是一个int类型的
在Golang中可以使用make关键字来声明通道
- 无缓冲通道 make(chan int)
- 有缓冲通道 make(chan int,2)(后面的参数2表示缓冲容量大小)
通道实质上就是切片(slice)
无缓冲通道与有缓冲通道的区别:
无缓冲通道会使得发送与接收的routine同步,也称为同步通道
解决方式就是采用有缓冲通道,缓冲容量表示通道中可以存放多少元素,类似于货架的格子
1.3Channel
示例——channel的具体使用:
Process A:发送数字0-9
Process B:计算其平方数
主协程打印计算后的平方数
package main
import (
"fmt"
)
func CalSquare() {
src := make(chan int) //这里创建了一个无缓冲的channel,src的意思是source,源
dest := make(chan int, 3) //这里创建了一个有缓冲的channel,缓冲大小为3,dest的意思是destination,目的地
go func() {
defer close(src) //这里是一个匿名函数,这个匿名函数会在协程结束时执行,这里的意思是在协程结束时关闭src
for i := 0; i < 10; i++ {
src <- i //将i写入src
}
}()
go func() {
defer close(dest) //这里是一个匿名函数,这个匿名函数会在协程结束时执行,这里的意思是在协程结束时关闭dest
for i := range src { //这里的意思是从src中读取数据,直到src关闭
dest <- i * i //将i*i写入dest
}
}()
//整体流程是这样的,首先开启了两个协程,一个协程向src中写入数据,一个协程从src中读取数据,然后将读取到的数据写入dest
for i := range dest { //这里的意思是从dest中读取数据,直到dest关闭
//消费者的操作可能会比打印慢一些,消费速度慢,但是带缓冲的channel可以解决该问题
fmt.Printf("%v\n", i)
}//主协程打印平方数
}
主要流程是在A中将i写入src(生产者),在B中读取src计算过后传入dest(消费者),主协程中打印dest中计算好的
多协程通常与匿名函数func(需要参数){函数体}(传入参数)相结合
这样可以保证顺序性,并发安全
1.4并发安全Lock
示例:对变量执行2000次++操作,5个线程并发进行
package main
import (
"fmt"
"sync"
"time"
)
var (
x int64
lock sync.Mutex //互斥锁
)
func addWithLock() {
for i := 0; i < 2000; i++ {
lock.Lock() //加锁
x = x + 1
lock.Unlock() //解锁
}
}
func addWithoutLock() {
for i := 0; i < 2000; i++ {
x = x + 1
}
}
func Add() {
//开启5个协程
x = 0
for i := 0; i < 5; i++ {
go addWithoutLock()
}
time.Sleep(time.Second)
fmt.Println("不加锁的结果:", x) //这里的结果是不确定的,每次运行的结果都不一样
x = 0
for i := 0; i < 5; i++ {
go addWithLock()
}
time.Sleep(time.Second)
fmt.Println("加锁的结果:", x)
}
x++的操作在每一个线程中可能并发进行,中间就可能有操作损失,而在加了互斥锁之后保证了操作的原子性,解决了高并发安全问题
并发安全问题有一定概率导致错误
1.5WaitGroup
在Golang中可以使用WaitGroup来实现并发任务的同步
三个方法:add,down,wait
实际上就是内部维护了一个计数器,计数器的值可以增加或减少
开启n个协程时,使计数器变为n,每完成一个协程计数器减一直到执行完
示例优化:
func hello(i int) {
fmt.Printf("Hello goroutine:%v\n", i)
}
func HelloGoroutine() {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func(j int) {
defer wg.Done() //defer是指在函数结束时执行,这里的意思是在协程结束时执行wg.Done(),使计数器减一
hello(j)
}(i)
}
wg.Wait()//这里的意思是等待计数器变为0,也就是等待所有协程执行完毕
}
在每个协程执行完后执行down(),再使用wait进行堵塞
2.依赖管理
依赖实际上是各种开发包
在实际开发中,我们不可能仅依靠原生标准库,还要有一些第三方依赖
2.1依赖管理引进
GoPATH→Go Vender→Go Moudule
不同环境(项目)依赖的版本不同
需要我们可以控制库的版本
2.1.1GoPATH
是Golang支持的一个环境变量,目录下有三个关键点
bin:项目编译生成的二进制文件
pkg:项目编译的中间产物,加速编译
src:项目源码
GOPATH的实现思路就是项目依赖src中的代码,于是就go get下载到最新版本包到src目录下
存在弊端:无法实现package的多版本控制
2.1.2 GoVender
项目目录下新增Vender目录,所有依赖包副本形式放在Project/Vender下
依赖寻址方式:Vender→GOPATH
通过每个项目引入一份依赖的副本,解决了package的多版本控制问题
问题:
问题:无法控制依赖的版本
更新项目可能出现依赖冲突
2.1.3 Go Molule
是Golang官方推出的依赖管理工具
通过go.mod文件管理依赖版本,使用go get/go mod指令工具管理依赖包
2.2依赖管理三要素
1.配置中心,描述依赖——go.mod
2.中心仓库管理依赖库——proxy
3.本地工具——go get,go mod
类似java里的maven,mod相当于pom
2.3.1 依赖配置
mod文件由三个部分组成:
依赖管理基本单元,实际上是模块,如果比较复杂的话每个包下面都要有一个mod文件
原生库,表示依赖的golang的原生库
单元依赖,由两部分组成,[Module Path][Version]
modpath,与上面定义的相对应
version:有一些版本规则:
语义化版本:{MINOR}${PATCH}
major大版本,互相之间不兼容
minor小版本更新,同一major下的不同minor之间兼容
patch bug修复
基于commit的伪版本:
vX.0.0-时间戳-哈希码
indirect关键字:
对于未直接依赖的包,我们就使用//indirect关键字表示间接依赖
incompatible关键字:
主版本2+模块会在模块路径下+vN后缀
对于没有go.mod文件并且主版本2+的依赖,会在后面+incompatible
2.3.2 依赖图
main依赖A的1.2和B的1.2,而这两个又分别依赖C的1.3和1.4,编译的时候用C的哪个版本?
实际上选1.4,会自动选择最低的兼容版本(1.3与1.4兼容)
2.3.3 依赖分发
实际上就是下载对应的依赖
可以直接在第三方仓库github上下载,但是有一些问题
为了解决这些问题,出现了proxy:
proxy是一个服务站点,缓存依赖内容,从proxy上拉取依赖
2.3.4 变量GOPROXY
通过GOPROXY环境变量来控制proxy的配置,是url列表,最后为direct
查找路径依次为url列表里的地址
2.3.5 工具go get
2.3.6 工具go mod
在开发中经常使用tidy命令,在每次提交代码时都可以用一下