这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天
从并发编程的视角了解go高性能的本质
并发指的是多线程程序在一个核cpu上运行,通过时间片的切换来实现同时运行的状态。
并行指的是直接利用多核来实现多线程的运行。
go语言实现了并发性能极高的调度模型。
协程:用户态,轻量级线程,栈KB级别。
线程:内核态,线程跑多个协程,栈MB级别。
协程使用
eg: 快速打印hello gorouine:
go中开启协程:
在调用函数时在函数前加上关键字go
func hello(i int) {
println("hell goroutine:" + fmt.Sprint(i))
}
func helloGoRoutine() {
for i := 0; i < 5; i++ {
go func(j int) {
hello(j)
}(i)
}
time.Sleep(time.Second)
}
为什么要在for循环外加上time.Sleep(time.Second)来做阻塞:
为了保证在协程退出前主线程不结束。
协程间的通信
go提倡通过协程间的通信来共享内存。而不是通过共享内存来实现通信。
通过通信来共享内存,涉及到了 channel 通道。
使用通道,相当于把协程做了一个连接。让一个routine发送特定的值到另一个routine。
Channel
Channel是一种引用类型,需要通过make来创建。
make(chan 元素类型,[缓冲大小])
缓冲大小为可选参数,根据有无分为无/有缓冲通道。
无缓冲通道也被称为同步通道。
WaitGroup
上面使用time.Sleep来做阻塞,因为不知道子协程确切的结束时间,因此就无法做一个精确的阻塞时间。
更优雅的方式是使用Sync包下的 WaitGroup来实现并发任务的同步。
WaitGroup提供了三个方法:
- Add(delta int) 计数器+delta
- Done() 计数器-1
- Wait() 阻塞直到计数器为0
如果启动了N个并发任务,可以将计数器增加N,每完成一个任务,计数器减一。最后调用Wait()方法阻塞,等待所有任务完成。
对上面代码用WaitGroup优化:
func ManyGoWait() {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func(j int) {
defer wg.Done()
hello(j)
}(i)
}
wg.Wait()
}
依赖管理
GOPATH->Go Vendor->Go Mudule
依赖管理的演进,主要是为了实现不同环境(项目)以来的版本不同
以及控制依赖库的版本
GOPATH
go语言支持的环境变量,是go项目的工作区。主要有三个关键点:
- bin 存放项目编译产生的二进制文件
- pkg 存放项目编译的中间产物,用来加速编译
- src 存放项目源码
项目代码直接依赖src下的代码
通过go get 下载最新版本的包到src的目录下
存在的问题:
如本地有projectA 和projectB。 都依赖同一个package。 package有两个版本v1和v2,实现了两个不同的方法。A依赖于v1的方法A,B依赖于v2的方法B。 但v2可能没有向下兼容,已经把v1中的A方法删掉了。
这时对于本地项目,因为依赖的是同一个package的源码,所以不能实现这两个项目同时构建成功。
无法实现package的多版本控制。
Go Vendor
项目目录下增加了vendor文件夹,所有依赖包以副本形式放在$ProjectRoot/vendor中。
会优先从vendor下寻找。
通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突的问题。
存在的问题:
projectA同时依赖于packageB和packageC,而且packageB和packagec又同时依赖了package D-v1和package D-v2。通过vendor的管理方式,就不能控制v1和v2的版本问题。
归根结底,出现这样问题的原因,是vendor依赖的仍然是项目的源码,不能清晰地表示项目的版本。
Go Module
通过go.mod文件管理依赖包版本
通过go get / go mod 指令工具管理依赖包
实现了定义版本规则和管理项目地依赖关系。
依赖管理三要素
- 配置文件,描述依赖 go.mod
- 中心仓库管理依赖库 Proxy
- 本地工具 go get / go mod
依赖配置
打开go.mod文件 长这样:
module github.com/wangkechun/go-by-example
go 1.18
require (
dependency latest
)
第一行为依赖管理基本单元。主要标识这个模块的路径,eg上的路径表示该项目托管在github上。
如果项目比较复杂,有很多package,那么每个package下都应该有一个mod文件。
go 1.18表示go的版本。
require 部分 表示单元依赖。每一个单元依赖由若干部分组成:
path 版本号
依赖配置-version
为了更好的做版本管理,go定义了自己的版本规则。
语义化版本:(主要来源于git tag)
{MINOR}.${PATCH}
MAJOR:大版本,不同的MAJOR版本可以不兼容
MINOR:新增函数或功能需要在MAJOR版本下做到兼容
PATCH:代码bug修复
基于commit伪版本
vx.0.0-yyyymmddhhmmss-abcdefg1234
indirect为非直接依赖
主版本2+模块会在模块路径增加/vN后缀。
对于没有go.mod文件并且主版本2+得到依赖,会+incompatible
在两个版本兼容时,会选择满足本次构建的最低的兼容版本
依赖分发-回源
对于go.mod中定义的依赖,可以直接从github的对应仓库下载。(无法保证代码的可用性,稳定性,增加第三方压力)
所以出现了Proxy
依赖分发-Proxy
go proxy 其实是一个服务站点,会缓存源站中的网站内容。保证以来的稳定性。
配置:
GOPROXY="proxy1.cn,https://proxy2.cn,…"
url列表,用逗号分隔
direct表示源站
proxy1->proxy2->Direct
会一次从proxy1,2寻找依赖,如果都没找到,就去源站
go get 工具
用来下载依赖。
go mod 工具
测试
回归测试 手动测试终端(刷刷抖音看能不能用)
集成测试 对接口进行测试
单元测试 开发阶段对模块函数进行测试
规则:
- 测试文件以_test.go结尾
- func TestXxx(*testing.T)
- 初始化逻辑放到TestMain中
编写好测试后可以用 go test[flags][packages]运行