go依赖管理和测试 | 青训营笔记

56 阅读5分钟

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

go 语言进阶和依赖管理

go高并发和协程

go充分发挥了多核优势, 高效运行

协程是go实现高并发的重要机制,

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

线程: 内核态, 线程运行多个协程, 栈MB级别

因为协程相较于线程的显著优势, 使得go更加胜任高并发编程

 //协程简单例子    
    func hello(i int) {
        fmt.Println("hello go routine : " + fmt.Sprint(i))
    }
    func HelloGoRoutine() {
        for i := 0; i < 10; i++ {
            go hello(i)
        }
        //存在更优雅的方法优化
        time.Sleep(time.Second)
    }
 ​

channel通信

  1. go提倡通过通信共享内存而不是通过共享内存而实现通信

    通信共享内存和共享内存实现通信的区别

  2. go通过channel通道实现goroutine之间的通信, 保证收发数据的顺序

  3. go存在通过共享内存实现的通信, 使用这种方式的通信需要通过对互斥量进行加锁获取临界的权限, 在一定程度生影响程序的性能, 因此推荐通过channel实现goroutine之间的通信

  4. 无缓冲通道又被称为同步通道, 用以将发送和接收的goroutine保持同步

    解决同步问题的方法之一就是使用带有有缓冲区的有缓冲通道

    带缓冲的channel, 可以用于解决生产者消费者模型中, 生产速度不一带来的瓶颈

      //chan是一种引用类型, 变量存储的是一个地址, 与slice类似, 作为形参时传入,可以简单的认为是地址
      //无缓冲通道
      //channel := make(chan int)
      //有缓冲通道
      //channelBuf := make(chan int,2)
     /*
         target A:子协程发送0-9
         target B:子协程计算输入数字的平方
         主协程输出最后的平方数
     */
     ​
     func CalcNumChannel() {
         //无缓冲通道用以同步
         src := make(chan int)
         //main作为父线程, 视为消费者, 会对数据进行一系列复杂操作, 因此即可视为消费者消费速度稍微慢了一些, 而生产者的生产速度稍快
         dest := make(chan int, 3)
         //生产协程的子协程
         go func() {
             defer close(src)
             for i := 0; i < 10; i++ {
                 src <- i
             }
         }()
         //计算数字的子协程
         go func() {
             defer close(dest)
             for i := range src {
                 dest <- i * i
             }
         }()
     ​
         for i := range dest {
             fmt.Println(i)
         }
     }
     ​
    

并发安全Lock

因为并发安全很难定位, 对于不加锁的并发执行, 结果可能会出现很大波动, 有可能会导致一定的问题, 却又因为是并发问题很难复现和定位, 因此项目开发工程中应当尽量避免使用共享内存实现的非并发安全的读写操作

并发安全Lock这里整体与os中读者写者模型一致, 采用信号量, sync.Mutex保证并发安全

 package Lesson2
 ​
 import (
     "fmt"
     "sync"
     "time"
 )
 ​
 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)
 }

WaitGroup

因为无法准确地了解go 协程的执行时间, 所以手动的使用time.Sleep()浪费很大, 因此可以使用WaitGroup更优雅地等待子协程执行完毕

`WaiGroup.Add(delta int) 开启的协程计数器+1delta

WaitGroup.Done() 计数器-1

WaitGroup().Wait() 阻塞程序执行, 直至计数器为0

使用方式为

 var wg sycn.WaitGroup
 ​
 wg.Add(routineCnt)
 ​
 go func(){
     defer wg.Done()
     ...
 }
 ​
 wg.Wait()
 ​

依赖管理

GOPATH

go初期的包管理工具, 通过全局的gopath管理所有的第三方包

-->GOPATH

-->GOPATH-->src // 存放所有的第三方包的源代码

-->GOPATH-->bin //编译的二进制可执行文件

-->GOPATH-->pkg //存放编译好的第三方包的对象, 用于加速程序运行

go get xxx默认将第三方包下载到GOPATH/src

缺点很明显, 无法解决多个项目中, package的多版本控制

另, goPath是通过对package源码实现的依赖

Go Vendor

在项目目录中增加vendor文件, 所有依赖包副本放到projectRootPath/vendor/

在vendor机制中, 项目的依赖会优先从vendor文件夹中获取, 若是vendor不存在, 便访问上级, 取GOPATH中获取

因此, vendor解决了多项目对同一个package的多版本依赖问题

缺点也很明显, 当项目A依赖的package B, 和A同时依赖package C时, vendor无法控制依赖的版本, 更新项目可能会导致出现依赖冲突(即package C的两个版本不兼容, 导致编译错误)

造成缺点的主要原因是vendor是依赖的是package的源码, 无法很好的标志依赖的版本概念.

Go Module

  • 通过go.mod文件, 管理项目的依赖包版本

  • 通过go get/go mod指令工具管理依赖包

    go module可以定义版本规则(推测类似python, numpy>=x.x)

依赖管理三点要素##

  1. 配置文件, 描述依赖, 通过go.mod实现
  2. 中心仓库管理依赖库, Proxy (这里不明白, 看完要查一下)
  3. 本地工具 go get/mod

类似于maven, go.mod类似于maven标注依赖包版本信息的文件

version

go path 和go vendor是对package源码的依赖, 不能很好的辨识版本信息

而go module定义了相应的版本规则用于管理package version

主要分为语义化版本和基于commit的伪版本

eg: v1.1.2, 是语义化版本, 大版本之间代码隔离, 中版本之间是函数新增, 小版本是bug修复

eg: v1.0.0-20201130134442-hashCodePrefix

基于commit的伪版本即版本号-时间戳(精确到秒)-hash码的12位前缀

indirect(间接依赖), 对于间接依赖的package, 会在mod中标注example/lib1 v1.0.0 //indriect

incompatible, 对于早期项目的兼容性支持, 早期项目不存在go.mod, 并且版本已经更新至v2.0.0以上时, 会在版本后标注example/lib2 v2.2.0+incompatible

go在选择包的依赖版本时, 会选择最低的兼容版本

项目间接依赖了package c v1.3.0和package c v1.4.0,

而package c 存在v1.1-v.1.5, 此时go会选择满足所有package依赖的最低兼容版本

即maxof(packageC.version)

依赖分发

问题: 如果直接从GitHub等托管平台下载依赖包, 会存在作者对代码随意更改导致package失效, 并且会给托管平台带来超于预期的流量访问, 增加托管平台的压力

解决: go proxy

go Proxy

go的服务站点, 相当于一个镜像源, 缓存源站中的软件内容, 并且不会对代码进行改变

从go proxy直接拉取包依赖, 保证了依赖的稳定性

管理工具

go mod

go mod init 初始化, 创建go.mod文件

go mod download 下载模块到本地缓存

go mod tidy增加需要的依赖, 删除不需要的依赖

go get

image-20230116163536563

go 测试

单元测试

  • 所有测试文件以_test.go结尾

  • 初始化逻辑放到TestMain中

     func TestMain(m *testing.M)[
         //可以读取命令行等信息    
         //测试前: 数据装载, 配置初始化等前置工作
         code := m.Run()//运行pkg下的所有单元测试
         //测试后:释放资源
         os.Exit(code) 
     ]
    

assert

通过第三方assert包实现某些equal/notEqual方法

eg: assert.Equal(*testing.T,target,result)

覆盖率

衡量代码正确率等的指标, 代码覆盖率越高, 正确性越具有保证

覆盖率是按照测试单元代码覆盖行数进行计算的,

eg: 测试单元存在15代码行, 但是用例仅执行了3行结束, 此时覆盖率位20%

一般的单元测试覆盖率在50%-60%, 较高覆盖率80%+

测试时的tips:

测试分支相互独立, 全面覆盖

测试单元粒度足够小, 函数单一职责

依赖

要求尽量将单元测试保证幂等性和稳定性

Mock

monkey主要是通过unSafe包, 在运行时, 将原函数的地址替换为打桩函数的地址, 实现Mock的功能

  • 为一个函数打桩, 用函数B替换函数A, 则称函数B为打桩函数
 //替换为打桩函数
 monkey.Patch(targetFunc, func()string{
     return "xxx"
 })
 //将打桩函数撤回变为原函数
 defer monkey.Unpatch(targetFunc)

基准测试

测试程序性能以及CPU的损耗, 对代码进行性能分析

基准测试以Benchmark关键字开头

 func BenchamarkSelect(b *testing.B){
     init()
     //重置运行时间, 去除初始所消耗的时间 
     b.ResetTimer()
     //对目标函数进行串行的基准测试 
     for i:=0;i<b.N;i++{
         Select()        
     } 
 }
 ​
 //并行的基准测试
 func BenchamarkSelectParallel(b *testing.B){
     init()
     b.ResetTimer()
     b.RunParallel(func (pb *testing.PB){
         for pb.Next(){
             Select()
         }
     })
 }

以课程代码为例, rand()具有一把全局锁, 为了全局安全, 保证随机性等问题, 降低了并行的性能.

因此, 使用了fastrand对rand包进行了替代