这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天
go进阶
go可以发挥多核cpu的优势
协程:用户态,轻量级线程,栈KB级别
线程:内核态,线程跑多个携程,栈MB级别
协程间通信
通过通信共享内存
多个goroutine操作一片内存:数据硬态
channel
并发安全:内部使用了互斥锁
make(chan type,[size])
- 有缓冲通道:可以解决生产者和消费者速度不匹配问题
- 无缓冲通道:不设置size
并发安全
通过内存进行通信常用 需要使用锁等保证并发安全
WaitGroup
内部有计数器,统计并发任务的数量 ADD(),Done(),Wait()
Sync
- Mutex:互斥锁
- RWMutex:读写分离锁
- 不限制并发读,只限制并发写和读写
- WaitGroup:线程同步,协调主线程和子线程之间关系,防止main函数退出
- 等待一组goroutine返回
- 相似功能:sleep,管道,waitGroup
- Once: 保证某段代码只执行一次
- Cond:
- 让一组goroutine在满足特定条件时被唤醒
- 具有阻塞协程和唤醒协程的功能
//10个人赛跑,1个裁判发号施令
func race(){
cond :=sync.NewCond(&sync.Mutex{})
var wg sync.WaitGroup
wg.Add(11)
for i:=0;i<10; i++ {
go func(num int) {
defer wg.Done()
fmt.Println(num,"号已经就位")
cond.L.Lock()
cond.Wait()//等待发令枪响
fmt.Println(num,"号开始跑……")
cond.L.Unlock()
}(i)
}
//等待所有goroutine都进入wait状态
time.Sleep(2*time.Second)
go func() {
defer wg.Done()
fmt.Println("裁判已经就位,准备发令枪")
fmt.Println("比赛开始,大家准备跑")
cond.Broadcast()//发令枪响
}()
//防止函数提前返回退出
wg.Wait()
}
容器的并发安全
- 数组、slice、struct允许并发修改(可能会脏写),并发修改map有时会发生panic
- 如果需要并发修改map请使用sync.Map
tips:使用 go build、go run、go test 这些 Go 语言工具链提供的命令时,添加 -race 标识可以帮你检查 Go 语言代码是否存在资源竞争。
依赖管理
GOPATH
- GOPATH:go项目工作区,bin,pkg,src
- 项目代码直接依赖src下的代码
- go get下载最新版本到src目录
缺点:无法实现package的多版本控制 eg.版本升级后,包中没有所用的方法
Go Vendor
- 项目目录下增加vendor文件,所有依赖包以副本形式存放在该目录下
- 依赖寻址方式:vendor->GOPATH 通过依赖副本,解决了多个项目需要同一个package(不同版本)依赖的冲突问题
缺点:项目依赖的2个包依赖同一个不同版本的包
- 无法控制依赖版本
- 更新项目可能出现依赖冲突,导致编译出错
Go Moudule
- 通过go.mod文件管理依赖包版本
- 通过go get/go mod 指令工具管理依赖包
- 定义版本规则和管理项目依赖关系
依赖管理三要素
- 配置文件,描述依赖 go.mod
- 中心仓库管理依赖库 Proxy
- 本地工具 go get/mod
version
语义化版本:${MAJOR}.${MINOR}.${PATCH}
基于commit伪版本:vx.0.0-yyyymmddhhmmss-abcdefgh1234(12位hash码前缀)
indirect
间接依赖
incompatible
- 主版本模块会在模块路径增加/vN后缀
- 对于没有go.mod文件并且主版本2+的依赖,会在版本号后面+incompatible eg. example/lib5/v3 example/lib6 vxxx+incompatible
依赖分发
- 回源 go会选择最低的兼容版本
- Proxy 类似适配器 GOPROXY="proxy1.cn,https://proxy2.cn,…"
direct表示源站:p1->p2->direct
go env -w GOPROXY=https://goproxy.cn,direct
go get
go get example.org/pkg
- @update 默认
- @none 删除依赖
- @v1.1.2 tag版本,语义版本
- @23dfdd5 特定的commit
- @master 分支的最新commit 注:commit为版本控制中的概念,例如:git
go mod工具
- go mod init 初始化,创建go.mod文件
- go mod download 下载模块到本地缓存
- go mod tidy 增加需要的依赖,删除不需要的依赖
测试
类型
- 回归测试:模拟使用?
- 集成测试:功能层次
- 单元测试:面向开发阶段
单元测试
输入、输出、期望
规则
- 所有测试文件以_test.go结尾
- func Test_test.go(*testing.T)
- 初始化逻辑放到TestMain中,测试前:数据装载,配置初始化等->code := m.run()->测试后:释放资源
- 运行:go test [flags] [packages] / RUN xxx
- assert包有相等判断的包
代码覆盖率
测试覆盖率:go test a_test.go a.go --cover
- 一般覆盖率50-60,较高到80
- 测试分支相互独立、全面覆盖
- 测试单元粒度足够小,函数单一职责
依赖
Mock
常用开源包monkey:github.com/bouk/monkey 快速Mock函数:为一个函数打桩,为一个方法打桩
打桩:用函数A替换函数B,B原函数,A打桩函数,不需要依赖本地文件等
实现:在运行时通过unsafe包将内存中函数地址替换成运行时函数地址
基准测试
Benchmark开头
go test -bench=. go test -v(获得列出所有测试及其结果的详细输出)
go test .\array_test.go .\array.go --cover
项目实践
需求设计
代码开发
数据层Repository:Model
外部数据的增删改查,不考虑底层数据的存储
逻辑层Service:Entity
处理核心业务逻辑输出
视图层Controller:view
处理和外部的交互逻辑
测试运行
限流
限流是限制到达系统的并发请求数量,保证系统能够正常响应部分用户请求,而对于超过限制的流量,则通过拒绝服务的方式保证整体系统的可用性。
- 单机限流
- 分布式限流
常用限流方式
计数器
一段时间内,对请求进行计数
缺点:没有很好的处理单位时间的边界
tips:高并发场景锁不太使用,可用原子计数替代
滑动窗口
解决计数器存在的临界点缺陷
缺点:格子的数量影响着滑动窗口算法的精度,依然有时间片的概念,无法根本解决临界点问题。
漏桶限流
原理:一个固定容量的漏桶,按照固定速率流出水滴。
特点:
- 固定容量,出水速率固定常量
- 空桶则不需要流出水滴
- 可以以任意速率流入
- 若流入水滴超过了桶的容量,则流入的水滴溢出
缺点: 漏桶限制的是常量流出速率(即流出速率是一个固定常量值),所以最大的速率就是出水的速率,不能出现突发流量。
令牌桶限流
常用
令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送。
原理: 我们有一个固定的桶,桶里存放着令牌(token)。一开始桶是空的,系统按固定的时间(rate)往桶里添加令牌,直到桶里的令牌数满,多余的请求会被丢弃。当请求来的时候,从桶里移除一个令牌,如果桶是空的则拒绝请求或者阻塞。
特点:
- 令牌按固定速率入桶
- 桶满时,新添加的令牌被丢弃
- 若桶中令牌不足,则不会删除令牌,且请求将被限流
Redis + Lua分布式限流
单机版限流仅能保护自身节点,但无法保护应用依赖的各种服务,并且在进行节点扩容、缩容时也无法准确控制整个服务的请求限制。
而分布式限流,以集群为维度,可以方便的控制这个集群的请求限制,从而保护下游依赖的各种服务资源。
分布式限流最关键的是要将限流服务做成原子化,我们可以借助 Redis 的计数器,Lua 执行的原子性,进行分布式限流