这是我参与「第五届青训营 」笔记创作活动的第3天
- 并发的深入
-
- 协程Goroutine
-
- 通道Channel
-
- 锁与并发安全
-
- 线程同步WaitGroup
依赖管理
- Gopath
- Go Vendor
- Go Module
单元测试
- 单元测试概念和规则
- Mock测试
- 基准测试
项目实战
并发的深入
并发 多线程程序在一个核的cpu上运行 并行 多线程程序在多个核的cpu上运行 协程:用户态,轻量级线程,栈KB级别 线程:内核态,线程跑多个协程,栈MB级别
开启协程
函数前加关键字 go 开启goroutine
package main
import (
"fmt"
"time"
)
func HelloPrint(i int) {
// println("Hello goroutine : " + fmt.Sprint(i))
fmt.Println("Hello goroutine :", i)
}
// 效果就是快速且无序打印
func HelloGoroutine() {
for i := 0; i < 5; i++ {
// 匿名函数是为了保护变量(大概)
go func(j int) {
HelloPrint(j)
}(i)
}
// time.Sleep()的作用是:保证了子协程在执行完之前,主协程不退出。
time.Sleep(time.Second)
}
func main() {
HelloGoroutine()
}
CSP 提倡通过通信共享内存而不是通过共享内存而实现通信
有缓冲通道
无缓冲通道
操作符
<-用于指定通道的方向,实现发送or接收;
特别地,若未指定方向,则为双向通道;
ch <- v // 把 v 发送到通道 ch
v := <-ch // 从 ch 接收数据 并把值赋给 v
package main
import (
"fmt"
)
func CalcPow() {
src := make(chan int)
dest := make(chan int, 3)
// 子协程src发送0~9数字
go func() {
defer close(src) // 当子协程src结束的时候再关闭,减少资源浪费
for i := 0; i < 10; i++ {
src <- i
}
}()
// 子协程dest计算输入数字的平方
go func() {
defer close(dest)
// 通过 range 关键字来实现遍历读取到的数据
for i := range src {
dest <- (i * i)
}
}()
// 主协程输出最后的答案
// 这里可以暂时认为子协程需要使用匿名函数
for i := range dest {
// 因为主协程可能会有更多的复杂操作,比较耗时,所以用带缓冲的通道可以避免问题
fmt.Println(i)
}
}
func main() {
CalcPow()
}
补充细节: 如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞
init函数补充说明:
程序初始化顺序:变量初始化 -> init() -> main()
并发安全
sync 包 下的 lock waitgroup
mutex 互斥锁实现线程同步
package main
import (
"fmt"
"sync"
"time"
)
// 除了使用channel实现同步之外,还可以使用Mutex互斥锁来实现同步。
var (
x int64
lock sync.Mutex
)
func AddWithLock() {
for i := 0; i < 2000; i++ {
lock.Lock()
x += 1
lock.Unlock()
}
}
func AddWithoutLock() {
for i := 0; i < 2000; i++ {
x += 1
}
}
func Add() {
x = 0
for i := 0; i < 5; i++ {
go AddWithoutLock()
}
time.Sleep(time.Second)
fmt.Println("WithoutLock :", x)
x = 0
for i := 0; i < 5; i++ {
go AddWithLock()
}
time.Sleep(time.Second)
fmt.Println("WithLock :", x)
}
func main() {
Add()
}
/*
Output:
WithoutLock : 8014
WithLock : 10000
*/
waitGroup 实现线程同步
package main
import (
"fmt"
"sync"
)
func HelloPrint(i int) {
fmt.Println("Hello WaitGroup :", i)
}
func ManyGoWait() {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func(j int) {
defer wg.Done()
HelloPrint(j)
}(i)
}
wg.Wait()
}
func main() {
ManyGoWait()
}
依赖管理
依赖演进 gopath govendor go module
GOPATH
是一个环境变量,其中有三个部分:
- bin:项目编译的二进制文件
- pkg:项目编译的中间产物,加速编译
- src:项目源码,项目代码直接依赖src下的代码
go get下载最新版本的包到src目录下
存在的弊端:无法实现package的多版本控制
Go Vender
项目目录下增加vender文件,所有依赖包副本形式放在$ProjectRoot/vender
依赖寻址方式:vender -> GOPATH
通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题。
存在的弊端:更新项目的时候可能导致编译错误的冲突;无法控制依赖的版本。
Go Module(1.16以后默认开启)
- 通过
go.mod文件管理依赖包版本 - 通过
go get/go mod指令工具管理依赖包
终极目标:定义版本规则和管理项目依赖关系
依赖管理三要素
- 配置文件 描述依赖 go.mod
- 中心仓库管理依赖库 proxy
- 本地工具 go get/mod
依赖标识:[module path][version/presudo-version]
version的两种类型
- 语义化版本
- 基于commit伪版本
indirect
对于没有直接表示的模块会在go.mod中加上// indirect,例如(A->B->C)
incompatible
- 主版本2+模块会在模块路径增加
/vN后缀 - 对于没有
go.mod文件并且主版本2+的依赖,会加上+incompatible,表示可能会存在一些不兼容的代码逻辑
一个例子:
选择最低的兼容版本
依赖分发-回源
依赖要去哪里下载,如何下载的问题。实际上是用Proxy来缓存,保证了依赖的稳定性;
依赖分发 变量-goproxy
GOPROXY=https://goproxy.cn,direct
上面的即go拉取依赖的服务站点地址配置,按从左往右的优先级去查找,直到找不到会查找下一个,最后是direct(默认的源站)。一般来说建议配置一个国内镜像这样在下载依赖的时候速度会快很多。当依赖被拉取下来后会在本地进行管理。注意:如果项目有导包,一定要将依赖拉取到本地保证本地有这个依赖,否则直接在导包的位置导入一个包的URL地址也是会爆红的。
- Proxy保证了依赖的稳定性:本地依赖库,无需担心依赖作者、修改依赖版本导致项目不可用。
- Proxy保证了依赖可用性:即使依赖作者删除依赖,本地依赖依然可以保证项目的开发和运行。
测试
从上到下,覆盖率逐层变大,成本却逐渐降低
单元测试
- 所有测试文件以_test.go结尾
- func TestXxx(*testing.T)
- 初始化逻辑放到 TestMain 中
package main
import "testing"
func BenchmarkFib20(b *testing.B) {
for n := 0; n < b.N; n++ {
Fib(20)
}
}
func Fib(n int) int {
if n < 2 {
return n
}
return Fib(n-1) + Fib(n-2)
}
覆盖率
如何衡量代码是否经过了足够的测试 如何评价项目的测试水准 如何评估项目是否达到了高水准测试等级
mock 测试
monkey: https://github.com/bouk/monkey
这是一个开源的mock测试库,可以对method或者实例的方法进行mock
Monkey Patch的作用域在Runtime,
运行时通过Go的unsafe包能够将内存中函数的地址替换为运行时函数的地址,将待打桩函数或方法的实现跳转。
基准测试
基准测试是指测试一段程序的性能及耗费CPU的程度;
在实际的项目开发中,经常会遇到代码性能瓶颈,为了定位问题,经常要对代码做性能分;
这时就用到了基准测试,其使用方法与单元测试类似。
- 优化代码,需要对当前代码分析
- 内置的测试框架提供了基准测试的能力