Go语言工程实践 | 青训营

69 阅读5分钟

语言进阶——并发编程

并发VS并行

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

b523dcac412b5f59afca118430c76c7.png 并行:多线程程序在多个核的cpu上运行

939b818665d765be91a523ac0d3e616.png

go可以充分发挥多核优势,高效运行

协程VS线程

协程——用户态

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

线程——内核态

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

线程上可以并发的跑多个协程

进程

进程是应用程序的启动实例,是系统进行资源分配和调度的基本单位,每个进程都有独立的内存空间,不同进程通过进程间的通信方式来通信

线程

线程从属于进程,是进程中的一个实体,线程是CPU调度的基本单位,一个线程由线程ID、当前指令指针、寄存器集合和堆栈组成

线程不拥有自己的系统资源,它与同属于同一进程的其他线程共享进程所拥有的全部资源,多个线程之间通过共享内存等线程间的通信方式来通信,线程拥有自己独立的栈和共享的堆

协程

协程可以理解为轻量级线程,一个线程可以拥有多个协程,与线程相比,协程不受操作系统调度,协程调度器按照调度策略把协程调度到线程中执行,协程调度器由应用程序的runtime包提供,用户使用go关键字即可创建协程,这也就是GO在语言层面直接支持协程的含义

⭐进程的切换内容包含页全局目录内核栈硬件上下文,切换内容保存在内存

⭐线程的切换内容包含内核栈硬件上下文,切换内容保存在内核栈

⭐协程的切换内容是硬件上下文,切换内容保存在用户栈或堆

在实际过程中如何开启协程

只要在创建的函数前面加个go,就可以开启协程了

//快速打印hello goroutine: 0~hello goroutine:4
func hello(i int) {
  printfln("hello goroutine : " + fmt.Sprint(i))
}

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

协程的执行过程

每一个go里面的线程会从schedule()开始运行,并且schedule()方法开始时是在g0栈上执行,g0栈就是给g0协程在栈空间中分配的内存地址,用来记录函数调用、跳转的信息。

G0 是每次启动一个 M 都会第一个创建的 gourtine,G0 仅用于负责调度的 G,G0 不指向任何可执行的函数,每个 M 都会有一个自己的G0。在调度或系统调用时会使用 G0 的栈空间,全局变量的 G0 是 M0 的 G0

协程如何实现通信

提倡通过通信共享内存而不是通过共享内存而实现通信

Channel

make(chan 元素类型,【缓冲大小】

  • 无缓冲通道 make(chan int)
  • 有缓冲通道 make(chan int,2)

f3a13d3429aac52874b5e0a6468d9ac.png

waitgroup

f1d1f271df54bc87ad89c4160b86038.png

计数器

  开启协程+1;执行结束-1;主协程阻塞直到计数器为0

语言进阶——依赖管理

依赖的概念:编译程序不可能所有东西都自己写,我们会大量使用一些第三方的库来引入自己的代码

演进:

b41fbfb8caee3a8e89a7c31ae5744ca.png

gopath

  • go语言支持的一个环境变量 $GOPATH
  • 项目代码直接依赖src下的代码
  • go get 下载在最新版本的包到src目录下

弊端:无法实现package的多版本控制

go vendor

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

弊端

无法控制依赖的版本

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

go module

  • 通过go.mod文件管理依赖包版本
  • 通过go get/go mod指令工具管理依赖包
  • 终极目标:定义版本规则和管理项目依赖关系

go mod命令

命令作用
go mod init生成go.mod文件
go mod download下载go.mod文件中指明的所有依赖
go mod tidy整理现有的依赖
go mod graph查看现有的依赖结构
go mod edit编辑go.mod文件
go mod vendor导出项目所有的依赖到vendor目录
go mod why查看为什么需要依赖某模块
go mod verify校验一个模块是否被篡改过

依赖管理三要素:

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

当同时有两个版本时(兼容),最终执行的是版本低的那个

依赖分发-回源

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

测试

  • 单元测试
  • Mock测试
  • 基准测试

d674e2e8af8b4ea1bfa1702d12cbefd.png

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

测试一般分为:

  • 回归测试:A同学手动通过终端回归一些固定的主流程场景
  • 集成测试:对系统功能维度做测试验证
  • 单元测试:开发者对单独的函数、模块做功能验证

单元测试

规则:

  • 所有测试文件以 test.go 结尾
  • func TestXxx( *testing.T)
  • 初始化逻辑放到 TestMain 中

eg:

func HelloTom() string {
	return "Tom"
}

func TestHelloTom(t *testing.T) {
	output := HelloTom()
	expectOutput := "Tom"
	assert.Equal(t, expectOutput, output)
}

运行结果:

ok  	github.com/Moonlight-Zhao/go-project-example/test	(cached)

覆盖率

衡量代码是否经过了足够的测试;

评价项目的测试水准;

评估项目是否达到了高水准测试等级

eg:

判断是否及格的函数:

func JudgePassLine(score int16) bool {
	if score >= 60 {
		return true
	}
	return false
}

func TestJudgePassLineTrue(t *testing.T) {
	isPass := JudgePassLine(70)
	assert.Equal(t, true, isPass)
}

运行结果:

ok      command-line-arguments  0.315s  coverage: 66.7% of statements

依赖

2165a5b5637049fad683dbaa418344c.png外部依赖 => 稳定&幂等

实际功臣中复杂的项目,一般依赖较多,而单元测试需要保证稳定性和幂等性

稳定性:相互隔离,能在任何时间、任何环境运行测试

幂等性:每一次测试运行都应该产生与之前一样的结果
函数说明
ReadFirstLine()文件处理的函数

Mock

使用 Monkey 库,Monkey 是一个开源的 mock 测试库,可以对方法或者实例进行 mock

monkey:github.com/bouk/monkry

🍇快速Mock函数:

  • 为一个函数打桩
  • 为一个方法打桩

eg:对 ReadFirstLine 函数打桩测试,不在依赖本地文件

func ReadFirstLine() string {
	open, err := os.Open("log")
	defer open.Close()
	if err != nil {
		return ""
	}
	scanner := bufio.NewScanner(open)
	for scanner.Scan() {
		return scanner.Text()
	}
	return ""
}
func ProcessFirstLine() string {
	line := ReadFirstLine()
	destLine := strings.ReplaceAll(line, "11", "00")
	return destLine
}
func TestProcessFirstLineWithMock(t *testing.T) {
	monkey.Patch(ReadFirstLine, func() string {
		return "line110"
	})
	defer monkey.Unpatch(ReadFirstLine)
	line := ProcessFirstLine()
	assert.Equal(t, "line000", line)
}

基准测试

基准测试是指测试一段程序的运行性能及耗费 CPU 的程度。在实际项目开发中经常会遇到代码性能瓶颈,需要利用基准测试对代码做性能分析来定位问题。

  • 优化代码,需要对当前代码进行分析
  • 内置的测试框架提供了基准测试的能力

eg:随机选择执行服务器

import(
    "math/rand"
  )

var ServerIndex [10]int
 
func InitServerIndex() {
	for i := 0; i < 10; i++ {
		ServerIndex[i] = i+100
	}
}
 
func Select() int {
	return ServerIndex[rand.Intn(10)]
}

基准测试以 Benchmark 开头,输入参数为*testing.B,Parallel为多协程并发测试

func BenchmarkSelect(b *testing.B) {
	InitServerIndex()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		Select()
	}
}
func BenchmarkSelectParallel(b *testing.B) {
	InitServerIndex()
	b.ResetTimer()
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			Select()
		}
	})
}

可以看到代码在并发情况下存在劣化,主要原因是 rand 为了保证全局的随机性和并发安全,持有一把全局锁。解决这个问题——使用开源的高性能随机数方法 fastrand