第六届字节跳动青训营后端第二课 Go的进阶与依赖管理及测试| 豆包MarsCode AI 刷题

134 阅读7分钟

高并发

package main

import (
	"fmt"
	"time"
)

func hello(i int) {
	println("hello goroutine: " + fmt.Sprint(i))
}
func HelloGoRoutine() {
	for i := 0; i < 5; i++{
		go func(j int){
			hello(j)
		}(i)
	}
	time.Sleep(time.Second)
}
func main()  {
	HelloGoRoutine()
}
//go语言在调用函数前加入go关键字,就可以开启一个新的goroutine协程执行该函数
//主协程一结束默认终止协程,可以通过time.Sleep()来控制主线程的结束时间,但如此操作过于简陋,所以一般不使用
//这里还有新的语法点,在函数定义后加入括号,表示立即执行该匿名函数

Channel

Channel可以看作是Go语言中用于通信的消息队列,分为无缓冲通道和有缓冲通道两种

前者也被称为同步通道,通道的容量代表着通道中能存放多少元素。

后者能够解决生产者和消费者执行通道内容速度不匹配的问题,

创建格式:make ( chan 元素类型,[ 缓冲大小 ] )

package main
func CalSquare() {
	src := make(chan int) //无缓冲通道
	dest := make(chan int, 3) //有缓冲通道
	go func() {
		defer close(src)
		for i := 0; i < 10; i++ {
			src <- i
		}
	}() //生产者,发送0~9数字到src通道中
	go func() {
		defer close(dest)
		for i := range src {
			dest <- i * i
		}
	}() //消费者,从src通道中取值,计算平方后发送到dest通道中
	for i := range dest {
		println(i)
	}//主goroutine从dest通道中取值并打印
}
func main() {
	CalSquare()
}

WaitGroup

内部维护了一个计数器,可以增加(开启协程)或者减少(协程结束),可以使主协程阻塞直到计数器为 0 。即当所有协程任务执行完毕后, 主协程再执行完毕,避免提前关闭主协程导致子协程未完全执行。

创建格式:var 变量名称 sync.WaitGroup

.Add(n) :增加n个协程单位

.Done() :减去一个协程单位

.Wait() :阻塞主协程,直到计数器的协程数量清零

package main

import (
	"fmt"
	"sync"
)
func hello(i int) {
	println("hello goroutine: " + fmt.Sprint(i))
}

func ManyGoWait() {
	var wg sync.WaitGroup
	wg.Add(5)//5个协程,计数器增加5
	for i := 0; i < 5; i++{
		go func(j int){
			defer wg.Done()//协程执行完毕,计数器减1
			hello(j)
		}(i)
	}
	wg.Wait()//阻塞主协程,直到计数器为0
}
func main() {
	ManyGoWait()
}

Mutex

是一个互斥锁,在主流编程语言的多线程操作中都支持使用互斥锁。Go语言也保留了这种操作,但其并不提倡使用内存共享来实现通信,所以在Go开发中少用。

package main

import (
	"sync"
	"time"
)

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
	}
}
func Add() {
	x = 0
	for i := 0; i < 5; i++ {
		go addWithoutLock()
	}
	time.Sleep(time.Second)
	println("WithoutLock", x)
	x = 0
	for i := 0; i < 5; i++ {
		go addWithLock()
	}
	time.Sleep(time.Second)
	println("WithLock", x)
}
func main() {
	Add()
}

依赖管理

Go的依赖管理演进

GOPATH → Go Vendor → Go Module

不同环境(项目)依赖的版本不同,依赖管理的目的是控制依赖库的版本

GOPATH

GOPATH是Go语言支持的的环境变量,是Go项目的工作区,主要文件结构如下

bin:项目编译的二进制文件

pkg:项目编译的中间产物,加速编译

src:项目源码

项目代码直接依赖src下的代码,go get下载最新版本的包到src目录下

多个项目依赖于某一package的不同版本,会出现问题,因为GOPATH无法实现package的多版本控制

Go Vendor

在项目目录下增加vendor文件,所有依赖包副本形式放在$ProjectRoot/vendor

依赖寻址方式 vendor ⇒ GOPATH

通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题。

在多依赖且依赖package相同但依赖版本不同的情况下,会造成依赖冲突,相同package的不同版本在此情况下不兼容

Go Module

是Go官方推出的依赖管理系统,解决了各种依赖冲突问题

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

通过 go get/go mod 指令工具对依赖包进行管理

依赖管理的三要素

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

依赖配置 - go.mod

module example/project/app //依赖管理基本单元

go 1.16 //原生库

require ( //单元依赖
		example/lib1 v1.0.2
		example/lib2 v1.0.0 // indirect 表示间接依赖
		example/lib3 v0.1.0-20190725025543-5a5fe074e612
		example/lib4 v0.0.0-20180306012644-bacd9c7ef1dd// indirect
		example/lib5/v3 v3.0.2
		example/lib6 v3.2.0+incompatible //对于没有go.mod文件并且主版本为2+的依赖
)

依赖标识:[Module Path][Version/Pseudo-version]

依赖配置 - version

语义化版本

MAJOR.{MAJOR}.{MINOR}.${PATCH}

V1.3.0

V2.3.0

基于 commit 伪版本

vx.0.0-yyyymmddhhmmss-abcdefgh1234

v0.1.0-20190725025543-5a5fe074e612 v0.0.0-20180306012644-bacd9c7ef1dd

依赖选择

当多个项目(A、B)依赖于同个项目(C)的不同版本(V1.3、V1.4),而又有一个项目依赖于上述项目(A、B),则在编译时会优先选择最低的兼容版本(V1.4)

依赖分发 - 回源

若直接使用版本管理仓库下载依赖会有以下缺陷

  1. 无法保证构建的稳定性(增加/修改/删除软件版本)
  2. 无法保证依赖可用性(删除软件)
  3. 增加第三方压力(代码托管平台负载压力)

依赖分发 - Proxy

Proxy是一个服务站点,会缓存源站中的软件内容和版本

用Proxy可以实现稳定和可靠的依赖分发

Proxy是十分强大的依赖分发系统,可以多层使用提高稳定性和可靠性,在实战项目中有重要的作用和意义

依赖分发 - 变量 GOPROXY

配置格式:GOPROXY=”proxy1.cn,proxy2.cm,direct”

服务站点URL列表,用逗号分隔,“direct”表示源站

Go Module就是依据该列表配置实现对Proxy的管理

项目查找依赖时优先从给定列表的最左边的URL尝试获取依赖

工具 - go get

用法:go get example.org/pkg

参数:

@update:默认

@none:删除依赖

@v1.1.2:tag版本,语义版本

@23dfdd5:特定的commit

@master:分支的最新版本

工具 - go mod

用法:go mod

参数:

@init:初始化,创建go.mod文件

@download:下载模块到本地缓存

@tidy:增加需要的依赖,删除不需要的依赖

测试

质量就是“生命”,测试是避免事故的最后一道“屏障”

回归测试 → 集成测试 → 单元测试,越往后成本越低,但覆盖率越大

单元测试

单元测试流程

输入 → 测试单元 → 输出 → 校对结果

测试单元:函数、模块等具有输入输出能力的代码区块都可以作为测试单元

单元测试的作用

  1. 保证质量
  2. 提升效率

单元测试 - 规则

  1. 所有测试文件以 _test.go 结尾,便于识别和区分
  2. 测试函数命名规范 *func TestXxx(t testing.T) ,符合规范才能运行测试函数
  3. 初始化逻辑放到 **func TestMain(m testing.M) *中
//Hello.go
package main
func HelloTom() string {
	return "Jerry"
}
//Hello_test.go
package main

import "testing"

func TestHelloTom(t *testing.T) {
	output := HelloTom()
	expectOutput := "Tom"
	if output != expectOutput {
		t.Errorf("Expected %s do not match actual %s.", expectOutput, output)
	}
}

单元测试 - assert(断言)

//Assert.go
package main
func HelloTom() string{
	return "Tom"
}
//Assert_test.go
package main
import (
	"github.com/stretchr/testify/assert"//导入第三方校对包assert,没有的话要用go get下载
	"testing"
)

func TestHelloTom(t *testing.T) {
	output := HelloTom()
	expectOutput := "Tom"
	assert.Equal(t, expectOutput, output)//使用第三方包的方法校对结果
}

单元测试 - 覆盖率

衡量代码是否经过了足够的测试,对项目的测试水准进行量化,评估项目是否达到高水准测试等级

Go可以用 go test 测试文件.go 源文件.go —cover 进行覆盖率测试

//Judgement.go
package main

// JudgePassLine 判断分数是否及格
func JudgePassLine(score int16) bool {
	if score >= 60 {
		return true
	}
	return false
}
//Judgement_test.go
package main
import (
	"github.com/stretchr/testify/assert"
	"testing"
)

func TestJudgePassLineTrue(t *testing.T) {
	isPass := JudgePassLine(70)
	assert.Equal(t, true, isPass)//对>=60的分支进行测试
}

func TestJudgePassLineFail(t *testing.T) {
	isPass := JudgePassLine(50)
	assert.Equal(t, false, isPass)//对<60的分支进行测试
}
//此时覆盖率为100%

单元测试 - Tips

实际项目中较难做到100%覆盖率 一般覆盖率:50%~60%,较高覆盖率80%+

提高覆盖率的方法:

  1. 测试分支相互独立、全面覆盖
  2. 测试单元粒度足够小,函数单一原则

Mock测试

在项目中,单元可能依赖于文件、数据库、缓存等

外部依赖 ⇒ 稳定&幂等

幂等:重复运行一个测试案例,结果是一致的

稳定:单元测试相互隔离,可以实现在任意时间任何位置对任何函数进行测试

Mock机制,为函数或者方法打桩,替换原函数,使得函数不依赖本地文件测试

//ReadFile.go
package main

import (
	"bufio"
	"os"
	"strings"
)

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
}
////ReadFile_test.go
package main

import (
	"github.com/stretchr/testify/assert"
	"bou.ke/monkey"
	"testing"
)

func TestProcessFirstLine(t *testing.T) {
	monkey.Patch(ReadFirstLine, func() string {
		return "line110"
	})//生成打桩函数
	defer monkey.Unpatch(ReadFirstLine)//卸载打桩函数
	line := ProcessFirstLine()
	assert.Equal(t, "line000", line)
}

基准测试

测试一段程序运行时的性能和测算CPU的损耗,Go内置了测试框架进行基准测试,使用方法类似单元测试。

  1. 测试函数命名规范 *func BenchmarkXxx(b testing.B)
  2. windows环境下 cmd输入 go test -bench=".” 进行压力测试
//Benchmark.go
package main
import (
	"github.com/bytedance/gopkg/lang/fastrand"
	"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)]
}
func FastSelect() int {
	return ServerIndex[fastrand.Intn(10)]
}
//Benchmark_test.go
package main

import "testing"

func BenchmarkSelect(b *testing.B) {
	InitServerIndex()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		Select()//串行基准测试,rand
	}
}
func BenchmarkSelectParallel(b *testing.B) {
	InitServerIndex()
	b.ResetTimer()//并行基准测试,fastrand
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			FastSelect()
		}
	})
}