Go 语言进阶与依赖管理 | 青训营笔记

50 阅读5分钟

Go 语言进阶与依赖管理 | 青训营笔记

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

一、本堂课重点内容

  • 并发和并行
  • goroutine
  • channel
  • 单测

二、详细知识点介绍

并发和并行

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

2023-01-14-16-57-50.png

GO 实现并发的方式

实现并发的过程中最重要的是要解决两个问题:

  • 如何让多个任务并发执行
  • 如何让多个任务之间进行通信

go 实现任务并发执行通过 协程(goroutine) 来实现,实现任务之间通信通过 通道(channel) 来实现。

Goroutine

go 在并发上有着其他语义难以企及的优势。这是因为 go 语言的并发是基于协程(Goroutine)的,而不是基于线程的。

Goroutine 是 go 语言的并发体,它是一种比线程更轻量级的存在。它的调度是由 go 运行时进行管理的。它的调度是由 go 运行时进行管理的。我们一般把 Goroutine 称为 协程

2023-01-14-17-03-02.png

go 中的协程在 用户态 中进行,而不是内核态,所以协程的切换不需要内核态的切换,所以协程的切换比线程的切换要快。协程的切换是由 用户自己控制 的,而线程的切换是由操作系统控制的。

Channel

go 不是通过共享内存通信,而是通过通信共享内存。通道是一种特殊的类型,它可以让我们通过它来传递数据。通道是一种 引用类型,通道本身是没有数据的,它需要通过 make 函数来初始化才能使用。

2023-01-14-17-03-53.png

并发安全 Lock

通过加锁可以保证并发数据安全

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

WaitGroup

控制协程进行任务编排

  • Add 方法用于设置 WaitGroup 的计数值,可以理解为子任务的数量
  • Done 方法用于将 WaitGroup 的计数值减一,可以理解为完成一个子任务
  • Wait 方法用于阻塞调用者,直到 WaitGroup 的计数值为 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()
}

依赖管理

2023-01-14-18-25-41.png

GOPATH

2023-01-14-18-26-48.png

无法实现 package 的多版本控制

Vender

2023-01-14-18-29-08.png

更新项目又可能出现依赖冲突,导致编译出错。

Module

依赖三要素

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

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

2023-01-14-18-31-57.png

  • go mod:打包模块
  • go get:下载最新版的包到 src 目录下

依赖配置

  • version:版本号
  • indirect:间接依赖
  • incompatible:可能不兼容

依赖分发

2023-01-14-18-56-17.png

  • 无法保证构建稳定性:增加/修改/删除软件版本
  • 无法保证依赖可用性:删除软件
  • 增加第三方压力:代码托管平台负载问题

2023-01-14-18-56-37.png

go get

go get example.org/pkg

  • @update:最新版本,默认
  • @none:删除依赖
  • @v1.0.0:tag 版本,语义版本
  • @23dfdd5:commit 版本
  • @master:分支版本

go mod

  • go mod init:初始化 go.mod 文件
  • go mod download:下载依赖
  • go mod tidy:添加需要的依赖,删除不需要的依赖

单元测试

回归测试:软件测试类型,以确认新的程序或代码更改未对现有功能产生影响。 集成测试:软件测试类型,用于验证软件组件之间的接口是否正确。 单元测试:软件测试类型,用于验证软件组件是否正确。

2023-01-14-21-07-47.png

  • 测试文件以 _test.go 结尾
  • 测试函数以 Test 开头
  • 初始化逻辑使用 TestMain 函数
func TestMain(m *testing.M) {
    // 测试前:数据装载,配置初始化等
    code := m.Run()
    // 测试后:资源释放,数据清理等
    os.Exit(code)
}

单元测试的优点

  • 便于后期重构。单元测试可以为代码的重构提供保障,只要重构代码之后单元测试全部运行通过,那么在很大程度上表示这次重构没有引入新的 BUG,当然这是建立在完整、有效的单元测试覆盖率的基础上。
  • 优化设计。编写单元测试将使用户从调用者的角度观察、思考,特别是使用 TDD 驱动开发的开发方式,会让使用者把程序设计成易于调用和可测试,并且解除软件中的耦合。
  • 文档记录。单元测试就是一种无价的文档,它是展示函数或类如何使用的最佳文档,这份文档是可编译、可运行的、并且它保持最新,永远与代码同步。
  • 具有回归性。自动化的单元测试避免了代码出现回归,编写完成之后,可以随时随地地快速运行测试,而不是将代码部署到设备之后,然后再手动地覆盖各种执行路径,这样的行为效率低下,浪费时间。

优化单元测试

  • 测试分支回想独立,避免测试分支之间的耦合。
  • 测试单元粒度足够小,函数单一职责

测试依赖-Mock

打桩

2023-01-16-20-25-45.png

  • 幂等性:同样的操作,多次执行结果相同。
  • 稳定性:测试依赖的服务稳定性,如果依赖的服务不稳定,会导致测试失败。

基准测试

基准测试:软件测试类型,用于验证软件组件的性能。

  • Benchmark 开头:基准测试函数
  • Benchmark + Parallel:并行基准测试函数

rand 函数有全局锁,导致并行基准测试函数性能下降。 fastRand 函数没有全局锁,性能提升。

四、课后个人总结

  • go 实现任务并发执行通过 协程(goroutine) 来实现,实现任务之间通信通过 通道(channel) 来实现。
  • 并行安全 Lock 和 WaitGroup 控制协程
  • 依赖管理对于项目后期维护和扩展性非常重要
  • 单元测试和基准测试是保证代码质量的重要手段

五、引用参考