go进阶| 青训营笔记

52 阅读3分钟

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

go进阶

高性能go语言

Goroutine

  • 协程:用户态,轻量级线程
  • 线程:内核态,线程跑多个协程
go func(j int) {
    hello(j)
}(i)

CSP(communicating sequential processes)

提倡通过通信共享内存, 而不是通过共享内存通信

image-20230117153712054-16739410385411.png

Channel

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

  • 无缓冲: make(chan int) 导致两个goroutine同步化
  • 有缓冲: make(chan int, 2)

实例:

func calSquare() {
    src := make(chan int)
    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

var lock sync.Mutex
​
lock.Lock()
...
lock.Unlock()

WaitGroup

  • Add(delta int) 计数器加delta
  • Done() 计数器减1
  • Wait() 阻塞直到计数器为0

对于多个协程, 可以设置对应数量的计数器, 结束时计数器减一, 直到全部结束, 实现阻塞的效果

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

依赖管理

go依赖演进

GOPATH->go vendor->go module

GOPATH:

  • 环境变量$GOPATH
  • 目录bin: 存放编译的二进制文件
  • 目录pkg: 存放编译的中间文件, 加速编译
  • 目录src: 存放项目源码
  • 弊端: 多个项目依赖一个包的不同版本, 无法实现package的多版本控制

Go Vender:

  • 在项目目录下增加vendor目录, 优先查找vendor中的依赖包, 再查找$GOPATH
  • 弊端: 无法控制依赖的版本

Go Module:

  • 通过go.mod文件管理依赖包版本
  • 通过go getgo mod指令管理依赖包

依赖管理三要素

  • 配置文件描述依赖: go.mod
  • 中心仓库管理依赖库: proxy
  • 本地工具: go get/mod

依赖配置-go.mod

image-20230117163838497.png

  • 语义化版本: ${MAJOR}.${MINOR}.${PATCH}
  • 基于commit伪版本: vx.0.0-{时间戳}-{hash code}
  • // indirect: 标识非直接依赖
  • +incompatible: 由于go module是1.11版本提出的, 要求主版本2+的模块要在路径后增加/vN后缀. 对于之前已经打上2以上版本的仓库, 要在版本号后加上这个后缀

依赖分发-proxy

image-20230117165841350.png 在代码仓库之间加入一层proxy服务站点, 缓存依赖的软件, 可以增加依赖的稳定性

  • 变量GOPROXY

    环境变量GOPROXY="url1, url2, direct". 标识服务站点url的列表, direct标识源站, 当其它站点都没有时从源站获取

工具-go get/mod

image-20230117170810805.png

image-20230117170835543.png

go测试

回归测试, 集成测试, 单元测试(覆盖率变大, 成本降低)

单元测试

针对函数, 模块等的测试, 将数据输入到测试单元, 将输出和期望做比较

规则:

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

  • func TestXxx(*testing.T)

    func TestHellotom(t *testing.T) {
        output := hellotom()
        expectOutput := "tom"
        if output != expectOutput {
            t.Errorf("some error")
        }
    }
    
  • 初始化逻辑放到TestMain

    func TestMain(m *testing.M) {
        // 测试前工作
        code := m.Run()
        // 测试后工作
        os.Exit(code)
    }
    

可以使用assert包进行判断

assert.Equal(t, output, expectOutput)

覆盖率--评估单元测试

  • 衡量代码是否经过了足够测试
  • 评价项目的测试水准
  • 评估项目是否达到了高水准测试等级
go test judgment_test.go judgment.go --cover

判断标准: 测试过程中代码的执行比例

对各个分支都进行测试, 保证覆盖率

一般覆盖率: 50%-60%;

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

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

mock--解决单元测试依赖问题

单元测试需要依赖外部文件,数据库,redis等, 测试结果需要满足幂等性和稳定性, 用mock可以解决外部数据不一致的问题

使用monkey库的patch方法进行打桩, 将调用外部数据的函数替换为另一个, 拥有确定的输出

image-20230117181723170.png

基准测试

测试代码性能

规则: func Benchmarkxxx(b *testing.B)

func BenchmarkSelect(b *testing.B) {
    InitServerIndex() // 初始化工作不需要纳入测试范围
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        Select()
    }
}
​
func BenchmarkSelectParallel(b *testing.B) {
    InitServerIndex() // 初始化工作不需要纳入测试范围
    b.ResetTimer()
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            Select()
        }
    })
}