go并发与测试 | 青训营笔记

39 阅读3分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 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提供了三个方法:

  1. Add(delta int) 计数器+delta
  2. Done() 计数器-1
  3. 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项目的工作区。主要有三个关键点:

  1. bin 存放项目编译产生的二进制文件
  2. pkg 存放项目编译的中间产物,用来加速编译
  3. 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 指令工具管理依赖包

实现了定义版本规则和管理项目地依赖关系。

依赖管理三要素
  1. 配置文件,描述依赖 go.mod
  2. 中心仓库管理依赖库 Proxy
  3. 本地工具 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)

MAJOR.{MAJOR}.{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 工具

测试

回归测试 手动测试终端(刷刷抖音看能不能用)

集成测试 对接口进行测试

单元测试 开发阶段对模块函数进行测试

规则:

  1. 测试文件以_test.go结尾
  2. func TestXxx(*testing.T)
  3. 初始化逻辑放到TestMain中

编写好测试后可以用 go test[flags][packages]运行