Go语言工程实践 | 青训营笔记

81 阅读4分钟

u=307425308,1606123941&fm=26&fmt=auto.jpg

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

Go语言进阶

并发和并行

并发:并发意味着程序在单位时间内是同时运行的。

并行:并行意味着程序在任意时刻都是同时运行的。

5603d8bbb49343658b6b5b081e26f561.png

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)

输出结果:

QQ截图20220612123419.jpg

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构建命令后的大致过程图解

202002252102493.jpeg

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库进行优化,性能可以提升很多倍,在大多数情况都适用。