Go语言进阶与实践 | 青训营笔记

78 阅读4分钟

这是我参与「第五届青训营」伴学笔记创作活动的第 2 天

并发

协程Goroutine

概念区分

进程/线程

  • 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。

  • 线程是进程的一个执行实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。

一个进程可以创建和撤销多个线程,同一个进程中的多个线程之间可以并发执行。

并发/并行

  • 多线程程序在单核心的 CPU 上运行,称为并发;
  • 多线程程序在多核心的 cpu 上运行,称为并行。

并发与并行并不相同,并发主要由切换时间片来实现“同时”运行,并行则是直接利用多核实现多线程的运行,Go程序可以设置使用核心数,以发挥多核计算机的能力。

image.png

线程/协程

  • 线程:一个线程上可以跑多个协程,内核态,栈MB级别。

  • 协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程;用户态,轻量级的线程,栈KB级别。

image.png

优雅的并发编程范式,完善的并发支持,出色的并发性能是Go语言区别于其他语言的一大特色。


Go创建Goroutine

在函数调用前使用go关键字

func HelloGoRoutine() {
 for i := 0; i < 5; i++ {
  go func(j int) {
   hello(j)
  }(i)
 }


Go中通信机制Channel

Go 实现并发的方式是:“不是通过共享内存通信,而是通过通信共享内存。”

image.png

channel类型

make(chan 元素类型, [缓冲大小])

  • 无缓冲通道 make(chan int)
  • 有缓冲通道 make(chan int, 2)
    • 每次向 channel 发送数据时,都会将元素添加到队列中。 然后,接收操作将从队列中删除该元素。 当 channel 已满时,任何发送操作都将等待,直到有空间保存数据。 相反,如果 channel 是空的且存在读取操作,程序则会被阻止,直到有数据要读取。

image.png

func CalSquare() {
 src := make(chan int)     //无缓冲
 dest := make(chan int, 3) //有缓冲
 go func() {
  defer close(src)
  for i := 0; i < 10; i++ {
   src <- i /*将i发送到src协程*/
  }
 }()
 go func() {
  defer close(dest)
  for i := range src {
   dest <- i * i /*计算并将结果发送到dest协程*/
  }
 }()
 for i := range dest { //主协程遍历
  //复杂操作
  println(i)
 }
}


并发安全Sync包

Lock通过对临界区的权限的控制保证并发安全

var (
 x    int64
 lock sync.Mutex
)

func addWithLock() {
 for i := 0; i < 2000; i++ {
  //每次+= 1前获取临界区权限
  lock.Lock()
  x += 1
  //释放临界区权限
  lock.Unlock()
 }
}

func addWithoutLock() {
 for i := 0; i < 2000; i++ {
  //无保护
  x += 1
 }
}

因为并发进行,所以无法预测执行先后顺序,可能发生数据竞争,应避免对共享内存进行非并发安全的读写操作

避免数据竞争的三种方法:

1.不要写变量

2.避免从多个goroutine访问变量

3.允许很多goroutine去访问变量,但是在同一个时刻最多只有一个goroutine在访问

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()
}

依赖管理

依赖管理目标:1.不同环境(项目)依赖的版本不同; 2.控制依赖库的版本

Go依赖管理演进

环境变量$GOPATH

GOPATH对应的的工作区目录有三个子目录:

  • bin:保存编译后的可执行程序
  • pkg:保存编译后的包的目标文件
  • src:存储源代码

项目代码依赖src下的代码;包位于src目录下

GOPATH弊端:无法实现package的多版本控制

Go Vendor

  • 在项目目录下增加vendor文件,所有依赖包副本形式放在$ProjectRoot/vendor
  • 依赖寻址方式:vendor优先于GOPATH

通过每个项目引入一份依赖的副本,解决多项目需要同一个package的问题

Go Vendor弊端:1.无法控制依赖的版本;2.更新项目可能出现依赖冲突,导致编译出错

Go Module

  • 通过go.mod文件管理依赖包版本
  • 通过go get/go mod指令工具管理依赖包

目标:定义版本规则和项目依赖关系

依赖管理三要数:

1.配置文件,描述依赖 go.mod

2.中心仓库管理依赖库 Proxy

3.本地工具 go get/go mod


依赖配置

依赖配置原则:选择最低兼容版本

go.mod

image.png

version

image.png

indirect非直接依赖

image.png

incompatible

主版本2+模块会在模块路径增加/vN后缀,这能让go module按照不同的模块来处理同一个项目不同主版本的依赖。由于gomodule是1。11实验性引入所以这项规则提出之前已经有一些仓库打上了2或者更高版本的tag了,为了兼容这部分仓库,对于没有go.mod文件并且主版本在2或者以上的依赖,会在版本号后加上+incompatible 后缀

依赖分发

变量GOPROXY

GOPROXY是一个 Go Proxy 站点URL列表,可以使用“direct”表示源站。整体的依赖寻址路径,会优先从proxy1下载依赖,如果proxy1不存在,后下钻proxy2寻找,如果proxy2,中不存在则会回源到源站直接下载依赖,缓存到proxy站点中。

go get

go get example.org/pkg

在后面可以添加

  • @update 默认, 拉最新的
  • @none 删除依赖
  • @v1.1.2 tag 版本, 语义版本
  • @23dfdd 特定的 commit
  • @master 特定的分支

go mod 后面添加

  • init 初始化,创建go.mod文件
  • download 下载模块到本地缓存
  • tidy 增加需要的依赖,删除不需要的依赖

测试

回归测试,集成测试,单元测试

单元测试

规则:


  • 所有文件以_test.go结尾
  • func TestXxx(*testing.T)
  • 初始化逻辑放到TestMain中

以代码覆盖率评估测试


tips:

  • 一般覆盖率:50%~60% ; 较高覆盖率:80%
  • 测试分支相互独立,全面覆盖
  • 测试单元粒数足够小,函数单一职责

依赖: image.png


Mock

  • 为一个函数打桩
  • 为一个方法打桩