从并发编程的角度了解Go高性能的本质|青训营笔记

128 阅读4分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第二篇笔记

语言进阶

从并发编程的角度了解Go高性能的本质

并发VS并行

并发:多线程程序在一个核的CPU上运行

并行:多线程程序在多个核的CPU上运行

Goroutine

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

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

快速打印 hello goroutine

func hello(int i){
    println("hello goroutine : " + fmt.Sprint(i))
}

func HelloGoRoutine() {
    for i := 0; i < 5; i++ {
        go func(j int) {
            hello(j)
        }(i)
    }
    time.Sleep(time.Second)
}

CSP Communicating Sequential Processes

让一个Goroutine发送一个特定的值到另一个Goroutine的机制

通过共享内存实现通信必须用互斥量进行加锁,一定程度上容易影响程序性能

Channel

创建方法:make(chan 元素类型, [缓冲大小])

无缓冲通道 make(chan int)

有缓冲通道 make(chan int,2) 容量为2, 指通道中能够存放多少元素

defer : 延迟的资源关闭

并发安全 Lock

对变量做两千次+1操作,5个协程并发执行

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)
    println("WithoutLock:", x)
    x = 0
    for 1:= 0; i< 5; i++ {
        go addWithLock()
    }
    time.Sleep(time.Second)
    printl("WithLock:",x)
}

执行后:withoutlock:8382, withlock:10000

WaitGroup

通过Wait进行阻塞

依赖管理

Go依赖管理的演进路线

GOPATH -> GO Vendor -> Go Module

不同环境(项目)依赖的版本不同

控制依赖库的版本

GOPATH

一个Go项目的工作区

  • bin 项目编译的二进制文件
  • pkg 项目编译的中间产物,加速编译
  • src 项目源码

GOPATH 弊端

无法实现package的多版本控制

Go Vendor

项目目录下增加vendor文件,所以依赖包副本形式放在 $ProjecrRoot/vendor 依赖寻址方式:vendor => GOPATH 通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题

Go Vendor 弊端

版本可能不兼容

Go Module

通过 go.mod 文件管理依赖包版本

通过 go get/go mod 执行工具管理依赖包

终极目标:定义版本规则和管理项目依赖关系

依赖管理三要素

配置文件,描述依赖,包如何唯一定位 - go.mod

中心仓库管理依赖库 - Proxy

本地工具 - go get/go mod

依赖配置 - go.mod

go 1.16 原生库版本号

require:描述单元依赖,根据path和版本号唯一定位某个版本/提交

依赖配置 - version

版本规则,分为基于语义化版本和基于commit伪版本

语义

不同MAJOR间代码隔离,MINOR新增的函数或功能,PATCH代码bug的修复

V1.3.0来源于git中tag的概念

commit

伪版本+时间戳+哈希码前缀

依赖配置 - indirect

indirect - 用于标识非直接依赖

依赖配置 - incompatible

标识可能存在不兼容的代码逻辑

依赖配置 - 依赖图

B,选择满足本次构建的最低的兼容版本 image.png

依赖分发 - 回源

github:代码托管的系统平台

依赖分发 - Proxy

保证依赖稳定性,实现稳定和可靠的依赖分发

依赖分发 - 变量 GOPROXY

工具 - go get

工具 - go mod

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

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

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

测试

事故:

营销配置错误,导致非预期用户享受权益,资金损失

用户提现,幂等失效,短时间可以多次提现,资金损失

代码逻辑错误,广告位被占,无法出广告,收入损失

代码指针使用错误,导致APP不可用,损失

测试是避免事故的最后一道屏障

测试:

回归测试:手动通过终端回归场景

集成测试:对系统功能维度做测试验证

单元测试:面对测试开发阶段,开发者对单独的函数模块做功能验证

从上到下,覆盖率逐层变大,成本却逐层降低

单元测试

保证质量

提升效率

单元测试 - 覆盖率

左边是函数,右边是单测,通过go test 加上 cover参数,就能在运行test的同时计算出代码测试的覆盖率,这里是66.7%,意思为函数的前两行有被验证过,最后一行没有运行到所以没被测试到

修改后的下一张图,这次覆盖率到了100%

单元测试 - Tips

一般覆盖率:50%-60%,较高覆盖率80%+

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

测试单元粒度足够小,即要求函数是足够小的,函数单一职责

单元测试 - 依赖

单元测试的两个目标:幂等,稳定

幂等:重复运行一个测试的case时得到结果是一样的

稳定:单元测试是能够相互隔离的,能在任何时间,任何函数独立运行

单元测试 - 文件处理

单元测试 - Mock

monkey:开源测试包,

测试时调的其实是打桩函数

基准测试

优化代码,需要对当前代码分析

内置的测试框架提供了基准测试的能力

基准测试 - 例子

基准测试- 运行

基准测试 - 优化

性能提高

fastrand牺牲了一定的一致性,但提高了性能

项目实战

需求描述

社区话题页面

  • 展示话题(标题,文字描述)和回帖列表
  • 暂不考虑前端页面实现,仅仅实现一个本地web服务
  • 话题和回帖数据用文件存储 分层结构
  • 数据层:数据Model,外部数据的增删改查
  • 逻辑层:业务Entity,处理核心业务逻辑输出
  • 视图层:视图view,处理和外部的交互逻辑