1.Go语言进阶
1.1 并发与并行
并发指多个线程在单核CPU上运行,是逻辑上的“同时”,某一时间点,实际上只有一个线程运行; 并行指多个线程在多核CPU上运行,可以存在某一时间点,多个不同的线程同时运行。
1.2 Goroutine
Go中一个重要概念——协程(Goroutine)。协程可以理解为轻量级的线程,协程的调度有Golang在运行时管理,一个线程可以跑多个协程。协程栈为KB级别,线程栈级别为MB。 Golang中使用go关键字开启协程,同一个程序中的所有 Goroutine 共享同一个地址空间。
package main
import (
"fmt"
"time"
)
func main() {
for i:= 0; i < 5; i++ {
go func (j int) {
hello(j)
}(i)
}
time.Sleep(time.Second) //保证子协程执行完之前,主协程(即main)不退出
}
func hello(i int) {
fmt.Println("hello goroutine: " + fmt.Sprint((i)))
}
//每次运行结果为乱序输出
//output1:
//hello goroutine: 0
//hello goroutine: 1
//hello goroutine: 4
//hello goroutine: 2
//hello goroutine: 3
//output2:
//hello goroutine: 4
//hello goroutine: 1
//hello goroutine: 3
//hello goroutine: 2
//hello goroutine: 0
//......
//outputN:
//hello goroutine: 0
//hello goroutine: 4
//hello goroutine: 1
//hello goroutine: 2
//hello goroutine: 3
1.3 CSP(Communicating Sequential Processes)
Golang提倡通过通信共享内存而不是通过共享内存实现通信。实现通信共享内存涉及到通道(Channel)的概念,假设有两个协程G1和G2,G1通过通道发送数据到G2共享信息。
1.4 通道(CHannel
Golang中使用make(chan type, cacheSize)创建通道,cacheSize表示通道中缓存大小,如果没有,则表示该通道无缓冲。操作符<-用来实现通道的数据传输。for循环可用来遍历通道。
package main
import (
"fmt"
)
func main() {
src := make(chan int) //发送者
dst := make(chan int, 5) //接收者
NegtiveNum(src, dst)
for i:= range dst { // 从通道dst中取出取负数后的数并打印
fmt.Println(i)
}
}
func NegtiveNum(src chan int, dst chan int) { //将1~5取反
go func () {
defer close(src)
for i:=1; i<=5; i++ {
src <- i //将i发送到通道src
}
}()
go func () {
defer close(dst)
for i:= range src { //遍历通道
dst <- (-i) // 从通道src中取出i,取负数传入dst通道
}
}()
}
1.5 并发安全Lock
Golang中使用互斥锁(也叫排他锁)sync.Mutex对不同的协程加锁与解锁,防止多个协程在访问同一内存区时出现混乱。
为什么会出现混乱?假设有两个协程G1和G2都做同一个变量sum的加1运算,当G1获取当前sum的值并加1,此时G2也获取sum的值,但是如果G1还没有把计算后的结果写入,G2获得的sum值就为原始的值,造成两个协程都写入了相同的结果,这样在很多实际情况下不符合业务逻辑。
lock sync.Mutex //声明一个互斥锁
lock.Lock() //加锁
lock.Unlock() // 解锁
1.6 WaitGroup
WaitGroup也在Golang中的sync包下,WaitGroup能实现多个任务的同步,保证在并发环境中完成指定数量的任务。WaitGroup涉及的方法如下:
| 方法名 | 功能 |
|---|---|
| (wg * WaitGroup) Add(delta int) | 计数器+delta |
| (wg * WaitGroup) Done() | 计数器-1 |
| (wg * WaitGroup) Wait() | 计数器!=0时阻塞直到为0 |
2.依赖管理
实际开发过程中,只有标准库往往不能满足开发需求,Golang中使用sdk方式引入库,依赖管理从GOPATH到Go Vender再到Go Module演变,现在多使用Go Module管理库。依赖管理三要素包括配置文件go.mod, 中心仓库管理依赖库proxy, 本地工具go get/mod。
2.1 GOPATH
GOPATH分为三个文件夹bin、pkg和src管理项目,bin保存项目编译的二进制文件,pkg保存项目编译的中间产物,src保存项目源码,所有的项目代码直接依赖到src下的代码。GOPATH的弊端在于无法实现package的多版本控制。
2.2 Go Vender
项目目录下增加Vender文件,所有的依赖包以副本形式放在vender,go代码会优先从vendor目录先寻找依赖包;找不到再从GOPATH 中寻找。弊端在于无法精确的引用 外部包进行版本控制,不能指定引用某个特定版本的外部包,只是在开发时将其拷贝过来,但是一旦外部包升级,vendor 下面的包会跟着升级,而且 vendor 下面没有完整的引用包的版本信息, 对包升级带来了无法评估的风险。
2.3 Go Module
- 通过go.mod文件管理依赖包版本
- 通过go get/go mod指令工具管理依赖包
- go.mod文件记录了项目所有的依赖信息,其结构大致如下:
module example
go 1.20
require (
github.com/gin-gonic/gin v1.4.0
github.com/go-sql-driver/mysql v1.4.1
github.com/jmoiron/sqlx v1.2.0
github.com/satori/go.uuid v1.2.0
)
2.4 依赖配置
2.4.1 version
- 语义化版本:
$(MAJOR).S{MINOR.S[PATCH} - 基于commit伪版本:
vX.0.0-yyyymmddhhmmss-abcdefgh1234
2.4.2 indirect和incompatible
indirect表示非直接依赖,incompatible表示允许不同主版本(MAJOR)的不兼容
3.测试
3.1 单元测试
单元测试流程如下,测试单元中包含不同的模块、函数等等
graph TD
输入 --> 测试单元 --> 输出
输出 --> 校对
期望输出 --> 校对
Golang中测试命令为go test,有以下需要注意的点:
- 所有测试文件以_test.go结尾
- 测试函数格式为
func TestXxx(*testing.T) - 初始化逻辑放在
TestMain()中 - 通过覆盖率判断单元测试是否合格,参数为
--cover - 测试分支相互独立,测试单元粒度足够小(函数单一职责)
3.2 Mock测试
单元测试可能涉及到外部依赖,导致不稳定,这时候需要Mock测试。使用开源库Monkey打桩工具实现Mock。Mock将目标函数或方法的实现跳转到桩实现,避免依赖本地文件,注意事项有:
- Monkey不支持内联函数,在测试时
go test xxx -gcflags=-l禁用内联。 - Monkey不是线程安全的。
- Monkey使用
monkey.Patch(<target function>, <replacement function>)和monkey.Unpatch(<target function>)函数为测试单元打桩和解除桩。
3.3 基准(Benchmark)测试
基准测试的目的的是对代码的性能进行性能分析,Golang中提供了内置测试框架对代码进行基准测试。测试函数以BenchmarkXxx开头表示基准测试,命令为go test -bench=$dir$
import (
"fmt"
"testing"
)
func BenchmarkSprint(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
fmt.Sprint(i)
}
}
总结
Golang进阶学习中,个人觉得最重要的是协程的概念,go中协程的调度和管理是实现高并发高性能的重要一步,对于协程的通信提倡使用通道进行数据的传输,在sync包下实现了锁等机制,防止数据错乱;其次是项目的依赖管理,主要使用go get/mod对外部依赖包管理;不同的测试都向着高性能高稳定的方向进行,测试使用go test命令。