Go语言进阶与路径管理
2.1 语言进阶(从并发编程的视角了解Go高性能的本质)
- 01 并发与并行: 并发(多线程程序在一个核CPU上运行)、
并行(多线程程序在多个核的CPU上运行)。并行可以理解成实现并发的一个手段,Go语言可以充分发挥多核优势,高效运行。
- 1.1 Goroutine(协程),线程是系统中昂贵的资源,属于内核态,其创建、切换、停止都属于重的操作,栈MB级别。协程可以理解为轻量级的线程,属于用户态,栈KB级别。(开启协程的方法,在调用函数的时候在函数前面加上
go
关键字即可)// 快速打印hello goroutine:0~hello goroutine 4 func hello(i int){ println("hello goroutine :"+fmt.Sprint(i)) } func HelloGoRoutine(){ for i := 0; i<5; i++{ go func(j int){ hello(j) } } time.Sleep(time.Second) // 阻塞,为了保证协程结束前线程不退出 }
- 1.2 GSP(Comunicating Sequential Processes):协程之间的通信,Go提倡通过通信共享内存,而不是通过共享内存实现通信。前者:协程之间有通道连接。后者:通过共享内存
- 1.3 Channel 通过make关键字创建,
make(chan 元素类型, [缓冲大小])
- 无缓冲通道(两个GoRoutine之间无缓冲,发送接收同步)
make(chan int)
- 有缓冲通道(会阻塞发送)
make(chan int,2)
// A子协程发送0~9数字 // B子协程计算输入数字的平方 // 子协程输出最后的平方数 func CalSquare(){ scr := make(chan int) // 创建无缓冲chan dest := make(chan int,3) // 创建有缓冲通道 // 通过src这个chan实现A协程和B协程之间的通信 go func(){ defer close(src) for i := 0;i<10;i++{ src <-- i } }() go func(){ defer close(dest) for i:= range src{ dest <-- i*i // 这个什么运算符? } }() for i := range dest{ fmt.Println(i) } }
- 无缓冲通道(两个GoRoutine之间无缓冲,发送接收同步)
- 1.4 并发安全 Lock
// 对变量执行2000次+1操作,5个协程并发执行 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) fmt.Println("WithoutLock:",x) //8382 x = 0 for i := 0; i <5; i++{ go addWithLock() } time.Sleep(time.Second) fmt.Println("WithLock:",x) // 10000 }
- 1.5 WaitGroup, 前面使用time.Sleep实现阻塞不够优雅,Go中常用WaitGroup实现并发任务的同步,在
sync
包下面。三个方法分别是Add(delta int)
:计数器+delta、Done()
:计数器-1、Wait()
:阻塞直到计数器为0.实际上WaitGroup内部维护了一个计数器,开启协程计数器+1,执行结束-1;主协程阻塞直到计数器为0.// 快速打印hello goroutine:0~hello goroutine 4 func hello(i int){ println("hello goroutine :"+fmt.Sprint(i)) } func ManyGoWait(){ var wg sync.WaitGroup wg.Add(delta:5) for i := 0; i<5; i++{ go func(j int){ defer wg.Done() hello(j) } } wg.Wait() }
- 1.1 Goroutine(协程),线程是系统中昂贵的资源,属于内核态,其创建、切换、停止都属于重的操作,栈MB级别。协程可以理解为轻量级的线程,属于用户态,栈KB级别。(开启协程的方法,在调用函数的时候在函数前面加上
2.2 依赖管理(了解Go语言依赖管理的演进路线)
-
2.1 Go依赖管理演进:GOPATH----Go Vendor----Go Module,现在广泛使用的是Go Module。围绕以下两点
-
不同环境(项目)依赖的版本不同
-
控制依赖库的版本
-
GOPATH:是Go项目的工作区,bin:项目编译的二进制文件,pkg:项目编译的中间产物,加速编译,src:项目源码;项目代码直接依赖src下的代码;
go get
下载最新版本的包到src目录之下- GOPATH的弊端:假设项目A和项目B依赖于某个包的不同版本,无法实现package的多版本控制
-
Go Vendor:
- 在项目目录下增加了verdor文件,所有依赖包副本形式放在
$ProjectRoot/vendor
- 依赖寻址方式:vendor => GOPATH
- 通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题
- Go Vendor弊端:假设项目A依赖于package B和package C,B和C依赖于package D的两个版本,就会存在无法控制依赖的版本、更新项目出现依赖冲突,导致编译出错两个问题。
- 在项目目录下增加了verdor文件,所有依赖包副本形式放在
-
Go Module
- 通过go.mod文件管理依赖包版本
- 通过go get/go mod 指令工具管理依赖包
- 终极目标:定义版本规则和管理项目依赖关系
-
-
2.2 依赖管理的3要素
- 1、配置文件,描述依赖 g.mod
- 2、中心仓库管理依赖库 Proxy
- 3、本地工具 go get/mod
-
2.3
- 2.3.1 依赖配置-go.mod
module github.com/wangkechun/go-by-example //依赖管理基本单元 go 1.18 //原生库 require( //单元依赖 ~ )
- 2.3.2 依赖配置-version
- 语义化版本
${MAJOR}.${MINOR}.${PATCH}
,eg: V1.3.0 - 基于commit伪版本
vX.0.0-yyyymmddhhmmss-abcdefgh1234
- 语义化版本
- 2.3.3 依赖配置-indirect,A->B->C,A直接依赖B,间接依赖C,间接依赖会用indirect标识
- 2.3.4 依赖配置-imcompatible,
- 主版本2+模块会在模块路径增加/vN后缀
- 对于没有go.mod文件并且主版本2+的依赖,会+imcompatible标识
- 2.3.5 依赖分发-Proxy
- 保证了依赖的稳定性和可靠性
- 2.3.6 工具-go get
go get example.org/pkg
@updata
默认@none
删除依赖@v1.1.2
tag版本,语义版本@23dfdd5
特定的commit@master
分支的最新commit
- 2.3.7 工具go mod
-
init
初始化,创建go,mod文件 -
download
下载模块到本地缓存 -
tidy
增加需要的依赖,删除不需要的依赖
-
- 2.3.1 依赖配置-go.mod
2.3 测试(从单元测试实践出发、提升质量意识)
- 测试分为回归测试、集成测试、单元测试
- 回归测试:模拟用户使用
- 集成测试:以功能为主
- 单元测试
- 3.1 单元测试(输入————测试单元(函数、模块……)————输出————校对):保证质量、提升效率。
- 3.1.1 单元测试规则:
- 所有测试文件都以_test.go结尾
- 测试函数命名规范:func TestXxx(*testing.T)
- 初始化逻辑放到TestMain中,例如在测试前进行数据装载、配置初始化等前置工作,在测试后进行释放资源等收尾工作。
- 3.1.2 使用
go test xxx_test.go xxx.go
指令进行测试 - 3.1.3 可以使用开源assert包来实现对实际输出和期望输出的比较
- 3.1.4 单元测试————覆盖率(用来评估单元测试水准)使用
go test xxx_test.go xxx.go --cover
在单测的同时得到覆盖率,覆盖率就是指测试了代码的占比 - 3.3.5 单元测试————Tips
- 一般覆盖率:50%~60%,较高覆盖率80%+
- 测试分支相互独立、全面覆盖
- 测试单元粒度足够小,函数单一职责
- 3.1.1 单元测试规则:
- 3.2 测试单元————依赖(单元依赖于File、DB、Cache,要求实现测试是幂等、稳定的)
- 3.3 单元测试————文件处理
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") //将“11”替换为“00” return destLine }
func TestProcessFirstLine(t *testing.T) { firstLine := ProcessFirstLine() assert.Equal(t, "line00", firstLine) //这个测试是依赖于文件的,如果文件被篡改或者删除,那么这个测试将无法运行 }
- 3.4 单元测试————Mock(打桩)
- 打桩可以理解为用一个函数A去替换一个函数B,B就是原函数,A是打桩函数
- 对ReadFirstLine进行打桩测试,不再依赖本地文件
func TestProcessFirstLineWithMock(t *testing.T) { monkey.Patch(ReadFirstLine, func() string { // 打桩操作 return "line110" }) defer monkey.Unpatch(ReadFirstLine) // 在测试结束后卸载桩 line := ProcessFirstLine() assert.Equal(t, "line000", line) }
- 3.5 基准测试
- 优化代码,需要对当前代码进行分析
- 内置的测试框架提供了基准测试能力
- 基准测试函数以Benchmark开头,入参是
*testing.B
- 使用
go test -bench .
指令执行所有的基准测试文件
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)] }
输出为:func BenchmarkSelect(b *testing.B) { InitServerIndex() // init服务器列表 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() } }) } func BenchmarkFastSelectParallel(b *testing.B) { InitServerIndex() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { FastSelect() } }) }
goos: windows goarch: amd64 pkg: github.com/Moonlight-Zhao/go-project-example/benchmark cpu: AMD Ryzen 7 5800H with Radeon Graphics BenchmarkSelect-16 156641480 7.605 ns/op BenchmarkSelectParallel-16 33325927 34.97 ns/op BenchmarkFastSelectParallel-16 1000000000 0.4999 ns/op PASS ok github.com/Moonlight-Zhao/go-project-example/benchmark 3.936s
- 基准测试————优化(前面并行性能劣化的原因是rand锁,用fastrand)