Go语言进阶 | 青训营

108 阅读7分钟

语言进阶——并发视角

并发与并行的区别

并发主要通过时间片切换实现多线程同时运行的状态,在一个核的cpu上运行;而并行则是直接利用多核实现多线程的同时运行。

并行可以理解为实现并发(广义上的并发)的一个手段。

go语言实现了并发性能提高的调度模型,通过高效的调度,可以最大限度的利用计算资源,充分发挥多核计算机的优势。

Goroutine

线程:内核态,线程上可以并发的跑多个协程,栈MB级别; 协程:用户态,轻量级线程,栈为KB级别; 协程的调度和创建由go语言本身去完成,go一次可以创建上万左右的协程。

go语言开启协程的方式为,在函数前添加go关键字,便为函数添加了一个协程来运行。

协程同步?

CSP

协程间的通信为,通过通信共享内存,而不是通过共享内存来实现通信。(但也保留了后者的方式)

image.png Gorountine是程序并发的执行体,通道对执行体进行连接

共享内存,存在临界区,枷锁的问题,影响性能。

Channel

channel是引用类型,其创建要通过make关键字make(chan 元素类型, [缓冲大小])。 其中无缓冲通道的为make(chan int);有缓冲通道的为make(chan int, 2)

image.png 区别:使用无缓冲通道进行通道时,发送与接收的东西同步,故该通道也称为同步通道。使用有缓冲的通道解决同步问题。带缓冲的channel可以解决由生成者和消费者速度不同带来的执行效率问题。

并发安全Lock

加锁方式:

lock sync.Mutex

lock.lock()
xxxx(需加锁的资源,即临界区)
lock.Unlock() //释放临界区

不加锁则可能引发一些错误,且不易被定位,故在操作共享内存时要小心。

WaitGroup

内部维护了计数器:开启协程+1,执行结束-1,主协程阻塞直到计数器为0。

暴露了三个方法: Add(delta int)计数器+delta
Done()计数器-1
Wait()阻塞直到计数器为0

var wg sync.WaitGroup
wq.Add(5)
for...
    go func(){
    defer wg.Done()
    }
    wg.Wait()

依赖管理

依赖管理演进

GOPATH->Go Vendor->Go Module

GOPATH

Go里面的环境变量,Go项目的工作区

GOPATH原理:项目代码直接依赖src下的代码

弊端:无法实现多版本控制。因本地项目都依赖同一个源码,两个项目不可依赖同一个包的不同版本。

Go Vendor

在项目目录下增加一个vendor文件夹,vendor里存放了当前项目依赖的副本。寻找依赖时优先在vendor目录下寻找,找不到时再回溯到GOPATH目录下。

弊端:可能出现依赖冲突,导致编译出错。

Go Module

1.1版本引入 1.6版本默认开启。

通过go.mod文件管理依赖包版本,通过go get/go mod指令工具管理依赖包,实现定义版本规则和管理项目依赖关系。

依赖管理三要素

  1. 配置文件,描述依赖 go.mod
  2. 中心仓库管理依赖库 Proxy
  3. 本地工具 go get/mod

依赖配置:

  1. 依赖管理基本单元 module example/project/app //模块路径,标识了这个模块 如果项目较复杂,有很多包,每个包想单独被别人引入的话,需要在每个包下都创建一个go.mod文件
  2. 原生库 go 1.16
  3. 单元依赖 require( example/lib1 v1.0.2)module path + 版本号,唯一的定位一个仓库的某一个版本。

依赖配置-version

  1. 语义化版本${MAJOR}.${MINOR}.${PATCH}

    MAJOR:大版本,版本间代码隔离,不同MAJOR间可以不兼容

    MINOR:一些新增函数或功能,需要做到在一个MAJOR下前后兼容

    PATCH:做一个代码修复

  2. 基于commit伪版本vX.0.0-yyyymmddhhmmss-abcdefgh1234

    前缀:和语义化版本一样

    时间戳:提交commit时的时间戳

    哈希码前缀:提交commit时生成的哈希码前缀

依赖配置-indirect incompatible

对于没有直接import导入的依赖就会标识为非直接依赖,并用// indirect标识出来

对于没有go.mod文件并且主版本2+的依赖(go.mod还没引入时就已经被标识为版本2+了的),会加+incompatible后缀,标识出可能会存在不兼容

依赖图

image.png

此时最终编译所使用的C项目的版本会是v1.4——go进行版本选择的算法,会选择一个最低的兼容版本(首先保证v1.3与v1.4是兼容的)

依赖分发-回源

直接使用版本管理仓库下载依赖存在的问题:

  1. 无法保证构建稳定性
  2. 无法保证依赖可用性
  3. 增加第三方压力(代码托管平台负载问题)

依赖分发-Proxy

image.png proxy是一个服务站点,会缓存原站中的软件内容,缓存的软件版本也不会改变,通过proxy保证依赖的稳定性。(类似适配器)

依赖分发-变量 GOPROXY

go mod通过GOPROXY环境变量来控制proxy的配置。 GOPROXY是用逗号分割的url列表,依次寻找依赖;direct代表源站,若前面的url都没找到依赖则回源到源站。

工具-go get & go mod

go get会默认拉取MAJOR版本的最新提交。可选后缀如下:

  1. @update 默认
  2. @none 删除依赖
  3. @v1.1.2 tag版本,语义版本
  4. @23dfdd5 特定的commit
  5. @master 分支的最新commit

go mod:

  1. init 初始化,创建go.mod文件
  2. download 下载模块到本地缓存
  3. tidy 增加需要的依赖,删除不需要的依赖

测试

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

回归测试(手动走场景)——>集成测试(对暴露出的某个接口做一些自动化的回归测试)——>单元测试(开发阶段)

从左到右成本下降,覆盖率上升。故单元测试的覆盖率在一定程度上决定了代码的质量。

单元测试

  • 所有测试文件以_test.go结尾
  • func TestXxx(*testing.T)
  • 初始化逻辑放到TestMain中
    func TestMain(m *testing.M){
    //测试前:数据装载、配置初始化等前置工作
    code := m.Run()
    //测试后:释放资源等收尾工作
    os.Exit(code)
    }
    

覆盖率

go test xxxxx_test.go xxxx.go --cover即可得到单元测试覆盖率 一般覆盖率:50%-60%(主流程基本无问题),较高覆盖率80%+ 测试分支相互独立、全面覆盖 测试单元粒度足够小

单元测试——依赖

稳定(单元测试相互隔离,能在任何时间进行独立运行)&幂等(多次运行的结果一致)

Mock

Mock测试

若有外部依赖,可使用Mock测试,以保证单元测试的稳定性 monkey包:开源的mock测试包。实现主要是在运行时讲函数地址替换为打桩函数的地址。 为一个函数打桩 为一个方法打桩 Patch(target, replacement interface{} ) *PatchGuard target为待替换的函数 Unpatch(target interface{}) bool卸载替换的函数

func Testxxx(t *testing.T){
    monkey.Patch(ReadFirstLine, func() string{
        return "xxx"
    })
    defer monkey.Unpatch(ReadFirstLine)
    line := ProcessFirstLine()
    assert.Equal(t, "xxx", line)
}

基准测试

指测试一段程序运行时的性能和cpu运行时的损耗。对代码进行性能分析

//执行串行压力测试
func BenchmarkSelect(b *testing.B){
    xxx()
    b.ResetTimer()//时间重置
    for i :=0; i<b.N; i++ {
        Select()
    }
}
//并行执行
func BenchmarkSelectParallel(b *testinf.PB){
    xxx()
    b.ResetTimer()
    b.RunParallel(func(pb *testinf.PB){
        for pb.Next(){
            Select()
        }
    })
)

rand在高并发场景如果没有特别注意它的底层实现可能会造成一定的性能问题,推荐使用fastrand

项目实战

组件工具:Gin高性能go web框架 Go Mod

索引 通过将数据行映射成内存Map来实现内存索引

var (
    topicIndexMap map[int64]*Topic
    postIndexMap  map[int64][]*post
)
func initTopicIndexMap(filePath string) error {
    open, err := os.Open(filepath + "topic")
    if err != nil {
        return err
    }
    
    scanner :=bufio.NewScanner(open)
    topicTmpMap := make(map[int64]*Topic)
    for scanner.Scan(){
        text := scanner.Text()
        var topic Topic
        if err := json.Unmarshal([]byte(text), &topic); err != nil{
            return err
        }
        topicTmpMap[topic.Id] = &topic
    }
    topicIndexMap = topicTmpMap
    return nil
}

Router: 初始化数据索引

初始化引擎配置

构建路由

启动服务