1.并发编程
1.1 关于并发和并行
并发
:并发通常指的是在一个CPU下,多个进程任务通过时间片
的切换来在CPU上进行调度的一种模式。多线程程序在一个核上的CPU运行
并行
:多个进程任务同时进行,这个同时是真正意义上的同时,也就是两个进程任务同时进行,而不是通过时间片轮转抢占CPU的方式来进行的。多线程的程序在多个核的CPU上运行
1.2 协程
协程
,英文Coroutines,是一种比线程更加轻量级
的存在。正如进程可以拥有多个线程调度一样,线程也可以拥有多个协程,最重要的是,在系统级线程的机制下,线程的切换和调度都是在内核态下进行的,一旦切换到内核态,开销就会相当巨大,协程不是被操作系统所管理,而是完全由程序所控制(在用户态下执行),这样做的好处就是性能得到了很大的提升。
1.3 CSP(Communicating Sequential Process)
在golang中,提倡使用channel的通信共享内存
来实现通信,而不是共享内存而实现通信
1.4 Channel
channel1 := make(chan int, 5) //有缓冲的信道定义
channel2 := make(chan int) //无缓冲的信道定义
有缓冲通道和无缓冲通道的区别:无缓冲通道实际上会导致两个进程之间的通信同步化
,也就是传多少,接受多少,那么有缓冲通道的话就可以实现异步化
,假设业务场景是需要上传一个大容量的文件,假设没有缓冲区,那么用户需要一直等待,直到接受的进程接受文件完毕才能关闭发送进程。假设有缓冲区,那么发送进程就可以提前把大容量的文件先发送到缓冲通道中,然后缓冲通道再把文件发送到接受进程,这个过程就是异步。
channel使用示例
package main
import "fmt"
func CalSquare() {
src := make(chan int)
dst := make(chan int, 3) //定义缓冲区为10
go func() { //向生产者送数
defer close(src)
for i := 0; i < 10; i++ {
src <- i
}
}()
go func() { //消费者用数
defer close(dst) //在该函数结束后,自动关闭该信道
for i := range src {
dst <- i * i
}
}() //匿名函数,这个括号代表对函数进行调用
for i := range dst {
fmt.Println(i)
}
}
func main() {
CalSquare()
}
1.5 并发安全 Lock
简单来说就是解决临界区互斥的问题
package main
import (
"fmt"
"sync"
"time"
)
var (
x int64 //临界资源
lock sync.Mutex //锁
) //这个变量是全局的,任意线程均可以改变
func addWithLock() {
for i := 0; i < 2000; i++ {
lock.Lock() //互斥
x++
lock.Unlock()
}
}
func addWithoutLock() {
for i := 0; i < 2000; i++ {
x++
}
}
func testAdd() {
x = 0
for i := 0; i < 5; i++ {
go addWithoutLock()
}
time.Sleep(time.Second)
fmt.Println("如果没有对临界区加锁,计算得到的x是:", x)
x = 0
for i := 0; i < 5; i++ {
go addWithLock()
}
time.Sleep(time.Second)
fmt.Println("如果对临界区加锁,计算得到的x是:", x)
}
func main() {
testAdd()
}
1.6 WaitGroup 实现阻塞
上述实现阻塞的方法是不合理的,因为它是通过time.sleep强制进程进入阻塞状态的,而阻塞的时间是人为规定的
这个例子的需求只是阻塞主协程,子协程的执行顺序随意
package main
import (
"fmt"
"sync"
)
func hello(j int) {
fmt.Println(j)
}
func implFastAdd() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(j int) {
defer wg.Done() //完成一个协程的工作后 mutex -= 1
hello(j)
}(i)
}
wg.Wait() //主协程并发执行到这,检测mutex是否为0,不为0才继续向下执行,否则阻塞
fmt.Println("执行完毕!")
}
func main() {
implFastAdd()
}
2. 依赖管理
2.1 依赖管理三要素
- 配置文件,描述依赖 go.mod
- 中心仓库管理依赖库 Proxy
- 本地工具 go get/mod
可以类比与JAVA的Maven
2.2.1 依赖配置-version
语义化版本
${MAJOR}.${MINOR}.${PATCH}
MAJOR是代码隔离的部分,是不相兼容的
,MINOR是新增的函数等
,PTACH是分支
如 v1.3.0 v2.3.0基于commit的伪版本
如vx.0.0-yymmddhhmmss-abcdefg
2.2.2 依赖配置-indirect
对于非直接依赖项,用//indirect来标识出该项是非直接依赖的
2.2.3 依赖配置-incompatible
主版本在2+以上的模块会在模块路径增加/vN后缀
对于没有go.mod文件并且主版本2+的依赖,会+incompatible
2.2.4 依赖图
如果X项目依赖了A、B两个项目,且A、B分别依赖了C项目的v1.3、v1.4两个版本,最终编译时所使用的C项目的版本为如下哪个选项?
答案是1.4,因为要选最低的兼容版本,我们首先知道1.3和1.4是前后兼容的
,而B只能兼容1.4,A只能兼容1.3,因此最低兼容版本为1.4
2.3.1 依赖分发-回源
其实就是依赖的代码模块从哪下载的问题,一般都存在一个代码仓库,托管相关代码在平台上,在这些平台上,存在一些问题
- 无法保证构建的稳定性
代码作者可以在平台上增加/删除代码,很有可能因为代码作者对代码的改动而无法构建代码 - 无法保证依赖的可用性
与代码作者有关 - 增加第三方压力
代码托管平台压力负载的问题
2.3.2 依赖分发-GOPROXY
为了解决上述问题,用了go Proxy
机制
go Proxy提供了一个缓冲库,该缓冲库中保存、下载了代码,最终就可以直接从go Proxy中取依赖
一般定义为
GOPROXY = "https://proxy1.cn,https://proxy2.cn,direct"
//服务站点url,"direct"表示源站,当上述所有的url都无法连接时,就会回到第三方代码托管平台去下载代码
2.4 goMod工具
go get
go get example.org/pkg @update //默认抓取最新版本
@none //删除该依赖
@v1.1.2 //tag版本,语义版本
@23dfdd5 //特定的commit
@master //分支的最新版本
go mod
go mod init//初始化,创建go.mod文件
go mod download//下载模块到本地缓存
go mod tidy // 增加需要的依赖,删除不需要的依赖
3.测试
3.1 单元测试
3.1.1 测试命名规则
- 所有测试文件以_test结尾
- func TestXxx(t *testing T)
- 初始化逻辑放到TestMain中
3.1.2 单元测试实例
package main
import "testing"
func HelloTom() string {
return "Jerry"
}
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
if output != expectOutput {
t.Errorf("Error!%s dont match %s", output, expectOutput)
}
}
func main() {
}
3.2 单元测试-Mock
快速Mock函数
- 为一个函数打桩
- 为一个方法打桩
打桩
:实际上就是为方法/函数安装一个别名函数以此来避免对文件/网络/缓存的强依赖。