这是我参与「第三届青训营 -后端场」笔记创作活动的第一篇笔记
Go语言进阶
并发和并行
并发:并发意味着程序在单位时间内是同时运行的。
并行:并行意味着程序在任意时刻都是同时运行的。
Golang 的语法和运行时直接内置了对并发的支持。
Golang 里的并发指的是能让某个函数独立于其他函数运行的能力。当一个函数创建为 goroutine 时,Golang 会将其视为一个独立的工作单元。这个单元会被调度到可用的逻辑处理器上执行。Golang 运行时的调度器是一个复杂的软件,能管理被创建的所有 goroutine 并为其分配执行时间。这个调度器在操作系统之上,将操作系统的线程与语言运行时的逻辑处理器绑定,并在逻辑处理器上运行 goroutine。调度器在任何给定的时间,都会全面控制哪个 goroutine 要在哪个逻辑处理器上运行。
Goroutine
goroutine 特性:
(1)go 的执行是非阻塞的,不会等待。
(2)go 后面的函数的返回值会被忽略。
(3)调度器不能保证 goroutin 的执行次序。
(4)没有父子 goroutin 的概念,所有的 goroutin 是平等地被调度和执行的。
(5)Go 程序在执行时会单独为 main 函数 goroutin ,遇到其他go关键字时再去创建其他的 goroutine。
(6)Go 没有暴露 goroutine id 给用户,所以不能在 goroutine 里面显式地操作另一个goroutine 不过 runtime 包提供了一些函数访问和设置 goroutin 的相关信息。
协程 线程
协程:用户态,轻量级线程,栈MB级别。切换调用由Go语言本身完成,较线程轻量许多。
线程:内核态,线程跑多个协程,栈KB级别。线程的创建,切换,停止都属于很重的系统操作,较消耗资源。
实际开发过程中开启协程:
for i := 0; i < 5; i++ {
go func(j int) {
println("hello goroutine : " + fmt.Sprint(j))
}(i)
}
time.Sleep(time.Second)
输出结果:
5个同时运行,谁先运行完谁先输出。
Channel
用法
make(chan 元素类型, [缓冲大小])
有缓冲通道 make(chan int)
无缓冲通道 make(chan int,2)
func main() {
var src chan int
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 { //消费者1
dest <- i * i
}
}()
for i := range dest { //消费者2
println(i)
}
}
使用channel可使得生产效率高于消费效率,防止因等待消费而产生阻塞,从而减少阻塞等待消耗的时间。
并发安全
在进行高并发时,便需要考虑到并发安全的问题。
go提供了些办法保证并发安全:
互斥锁:go 语言中保留了通过共享内存实现通信的机制,这种情况会存在多个 goroutine 同时操作同一块内存资源的情况,可能导致并发安全的问题。这种情况需要通过加锁的方式解决并发安全问题。 Go语言标准库中可以用sync.Mutex实现。
计数器WaitGroup:代替time.Sleep(),保证所有线程运行完,且不会浪费时间。
Go依赖管理
Go依赖管理的发展过程
GOPATH -> Vendor -> GO MODULE
为什么要依赖管理
我们平时写项目,时常需要依赖一些第三方库,而随着项目越来越复杂,依赖的第三方库也越来越多,此时再依靠手动的去管理我们的依赖已经变得不现实,尤其是企业中的开发。因此我们就需要一些工具或者模式去帮我们更好的管理我们依赖。
Go Module
现在普遍用Go Module进行项目管理。
一般在创建的项目文件夹下用cmd输入go mod init (项目名)来创建一个go.mod文件,以此进行依赖管理。
go build构建命令后的大致过程图解
Go mod 其他命令
go mod download // 下载依赖的module到本地cache(默认为$GOPATH/pkg/mod目录)
go mod edit // 编辑go.mod文件
go mod graph // 打印模块依赖图
go mod init // 初始化当前文件夹, 创建go.mod文件
go mod tidy // 添加缺失的模块以及移除无用的模块
go mod vendor // 将依赖复制到vendor下
go mod verify // 验证依赖项是否达到预期的目的
go mod why // 解释为什么需要依赖
项目测试
测试可以保证运行质量,提升运行效率,避免事故,是十分重要的。
单元测试
测试文件以_test.go结尾,若是goland会自动识别是否为测试文件(vscode未试过)
func TestXxx(*testing.T)
初始化放入TestMain中。
该类型测试适用于没有外部依赖的代码内环境测试。
Mock测试
如果程序有外部依赖,在不同的测试环境,外部依赖信息可能会发生变化。比如程序需要打开某个文件,如果把相同的程序放在不同的环境测试,该文件的路径可能不一致。这就不符合稳定和幂等两个条件。
通过打桩(Mock)可以解决这个问题。打桩后,就不再依赖本地文件。
利用monkey库可以快速实现打桩。
基准测试
基准测试是指测试一段程序的运行性能及耗费 CPU 的程度。而我们在实际项目开发中,经常会遇到代码性能瓶颈,为了定位问题经常要对代码做性能分析,这就用到了基准测试。使用方法类似于单元测试。
基准测试优化: 利用字节跳动开发的fastrand库进行优化,性能可以提升很多倍,在大多数情况都适用。