Go语言进阶和依赖管理
并发VS并行
- 多线程在一个核的CPU上运行(假并发)
- 多线程在多个核的CPU上运行(真并发)
Go实现了一个并发性能极高的调度模型,通过调度来充分发挥多核优势,高效运行。
在Go里有个Goroutine的概念。
这里需要明确一下用户态和内核态的概念。
协程:用户态,轻量级线程,栈KB级别
线程:内核态,线程跑多个协程,栈MB级别
协程之间的通信:GO提倡的是通过协程之间的通信来贡献内存,而不是通过共享内存来实现协程之间的通信。
在goroutine之间传输数据就是使用channel实现的。通道就类似于一个队列,可以保证消息收发的顺序(我个人理解为就是一个mq)。
在这里就要提到进程之间传输数据的七种方式了:
- 管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
- 命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
- 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。 (我个人认为channel就很类似于这种思想)
- 共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
- 信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
- 套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
- 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
channel
cjammel是一种引用类型,使用make
关键字初始化,make(chan 元素类型,[缓冲大小])
- 无缓冲通道
make(chan int)
- 有缓冲通道
make(chan int,2)
2 表示缓冲通道的大小
func calSquare() {
src := make(chan int)
dest := make(chan int, 3)
go func() {
for i := 0; i < 10; i++ {
src <- i
}
defer close(src)
}()
go func() {
for i := range src {
dest <- i * i
}
defer close(dest)
}()
for i := range dest {
println(i)
}
}
Go 也保存通过共享内存来通信的方式
这种方式的话就存在一定的并发安全问题,对于这个问题就引入了lock sync.Mutex
来解决,在需要操作共享内存的时候先加锁lock.Lock()
,操作完共享变量之后再解锁lock.Unlock()
。
并发任务同步
在多并发任务中需要进行任务同步的时候,就需要用到WaitGroup
。在WaitGrou
中主要是三个方法Add Done Wait
.
- Add: 计数器加n
- Done :计数器-1
- Wait:阻塞直到计数器为0
func wait() {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func(j int) {
defer wg.Done()
}(i)
}
wg.Wait()
}
依赖管理
现在使用的依赖管理是go module
GOPATH
- bin:项目编译的二进制文件
- pkg:项目编译的中间产物,加速编译
- src:项目源码
项目代码直接依赖src下的代码
go get 下载最新版本的包到src下。
采用这种方式进行依赖管理的话不能够实现不同的项目依赖于同一个package的不同版本
GO Vender
项目目录下增加vendor文件,所有依赖包副本形式放在工程目录的vendor下。
依赖寻址方式 vendor => GOPATH
这种方式通过每个项目引入一份依赖副本,解决了多个项目需要同一个package依赖的冲突问题,但是还是存在依赖版本控制的问题,更新项目又可能出现依赖冲突,导致编译错误。
Go Module
通过go.mod
文件管理依赖包管理
通过go get/go mod
指令工具管理依赖包
终极目标,定义版本规则和管理项目依赖关系
单元测试
在go中,单元测试需要遵循一定的规则:
-
所有的测试文件以_test.go结尾
-
func TestXxx(*testing.T)
-
初始化逻辑放到TestMain中
func TestMain(m *testing.M){ //测试前,数据装载,配置初始化等前置工作 //测试后,释放资源等收尾工作 }
go test aaa_test.go aaa.go --cover
使用测试的时候如果需要查看代码单元测试的覆盖率,就在命令结尾加--cover
为了保证单元测试的幂等性和稳定性,不过分依赖于外部依赖(File、DB、Cache)等,就需要使用到Mock
使用mokey这个框架可以实现函数的打桩,也就是替换目标函数的方法。这样就保证了不依赖于外部环境进行测试。