GO工程实践 | 青训营笔记

86 阅读6分钟

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

go进阶

go可以发挥多核cpu的优势

协程:用户态,轻量级线程,栈KB级别

线程:内核态,线程跑多个携程,栈MB级别

协程间通信

通过通信共享内存

多个goroutine操作一片内存:数据硬态

channel

并发安全:内部使用了互斥锁

make(chan type,[size])
  • 有缓冲通道:可以解决生产者和消费者速度不匹配问题
  • 无缓冲通道:不设置size

并发安全

通过内存进行通信常用 需要使用锁等保证并发安全

WaitGroup

内部有计数器,统计并发任务的数量 ADD(),Done(),Wait()

Sync

  1. Mutex:互斥锁
  2. RWMutex:读写分离锁
    • 不限制并发读,只限制并发写和读写
  3. WaitGroup:线程同步,协调主线程和子线程之间关系,防止main函数退出
    • 等待一组goroutine返回
    • 相似功能:sleep,管道,waitGroup
  4. Once: 保证某段代码只执行一次
  5. 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

依赖分发

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:高并发场景锁不太使用,可用原子计数替代

滑动窗口

解决计数器存在的临界点缺陷

缺点:格子的数量影响着滑动窗口算法的精度,依然有时间片的概念,无法根本解决临界点问题。

漏桶限流

原理:一个固定容量的漏桶,按照固定速率流出水滴。

image.png 特点:

  • 固定容量,出水速率固定常量
  • 空桶则不需要流出水滴
  • 可以以任意速率流入
  • 若流入水滴超过了桶的容量,则流入的水滴溢出

缺点: 漏桶限制的是常量流出速率(即流出速率是一个固定常量值),所以最大的速率就是出水的速率,不能出现突发流量。

令牌桶限流

常用

image.png 令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送。

原理: 我们有一个固定的桶,桶里存放着令牌(token)。一开始桶是空的,系统按固定的时间(rate)往桶里添加令牌,直到桶里的令牌数满,多余的请求会被丢弃。当请求来的时候,从桶里移除一个令牌,如果桶是空的则拒绝请求或者阻塞。

特点:

  • 令牌按固定速率入桶
  • 桶满时,新添加的令牌被丢弃
  • 若桶中令牌不足,则不会删除令牌,且请求将被限流

Redis + Lua分布式限流

单机版限流仅能保护自身节点,但无法保护应用依赖的各种服务,并且在进行节点扩容、缩容时也无法准确控制整个服务的请求限制。

而分布式限流,以集群为维度,可以方便的控制这个集群的请求限制,从而保护下游依赖的各种服务资源。

分布式限流最关键的是要将限流服务做成原子化,我们可以借助 Redis 的计数器,Lua 执行的原子性,进行分布式限流