这是我参加青训营的第 2 天
工程实践
语言进阶
GoRoutine
在Golang语言机制中,实现高并发需要一个重要概念,协程(Goroutine)。
协程:属于用户态,轻量级线程,他的创建、完成由Golang语言本身去调动,比线程轻量很多。栈内存在KB级别
线程:比较昂贵的系统资源,属于内核态。他的创建,切换,停止都属于很重的系统操作,比较消耗资源。栈内存在MB级别。线程可以并发的跑多个协程
协程的使用(快速打印)
只需要在调用函数时,在函数的前面加一个go关键字,这就为一个函数创建了一个协程来运行
time.Sleep(time.Scond) :使用暴力的sleep方法来保证子线程执行完之前,主线程不退出
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.Scond)
}
CSP
协程之间的通信,Golang提倡使用通信来共享内存,而不是使用共享内存来实现通讯。但是Golang本身也还保留了使用共享内存来实现通讯的机制。
通道(Channel)
左图是通过通讯来实现共享内存的示意图
通道就相当于将协程做了一个连接,传输队列。遵循先进先出,保证收发数据的顺序
通道实现的是从一个Goroutine发送数据到另一个Goroutine中
右图是通过共享内存实现通讯的示意图
Channel
使用make创建缓冲通道,格式:make(chan 元素类型, [缓冲大小])
由因为缓冲大小,可以将缓冲通道分为无缓冲通道和有缓冲通道
· 无缓冲通道 make(chan int)
无缓冲通道可以实现发送的Goroutine和接收的Goroutine同步化,所以又被称为同步通道
· 有缓冲通道 make(chan int, 2)
有缓冲通道是异步的,它不会保证顺序性,所以又被称为异步通道
func CalSquare() {
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 {
dest <- i * i
}
}()
for i := range dest {
println(i) // 结果为依次输出 0 ~ 9 的相乘
}
}
并发安全Lock
当执行高并发程序时,我们可能在并发中失去准确性,所以要使用Lock来保证程序的安全性和完整性。
lock.Lock() 通过临界区控制实现加锁
lock.Unlock() 当操作执行结束之后,释放临界区资源,解除加锁。
var (
x int64
lock sync.Mutex
)
func addWithLock() {
for i := 0; i < 100; i++ {
lock.Lock()
x += 1
lock.Unlock()
}
}
func addWithoutLock() {
for i := 0; i < 100; i++ {
x += 1
}
}
当上述程序调用并且执行完毕之后,加锁的函数会完整的返回所需要的结果,没有加锁的会出现失真。
WaitGroup
Golang语言中,我们不知道子协程一个确切的结束时间,所以不能的设置睡眠时间,往前只能使用Sleep这种暴力方法实现主程序等待。
在学习WaitGroup之后,我们可以合理的使用WaitGroup来准确的控制主程序的等待时间。
使用WaitGroup来实现并发程序的同步
三个方法:
· Add(delta int) 实现计数器+delta
· Done() 实现计数器-1
· Wait() 阻塞直到计数器为0
以往例子改进:
func hello(i int) {
println("hello goroutine : " + fmt.Sprint(i))
}
func HelloGoRoutine() {
var wg sync.WaitGroup
wg.Add(5) // 因为5个协程
for i := 0; i < 5; i++ {
go func (j int) { // 协程
defer wg.Done() // 子协程完成一个,计数器-1
hello(j)
}(i)
}
wg.Wait()
}
依赖管理
目前广泛应用的为 Go Module
GOPATH 弊端: 无法实现package的多版本控制
Go Vendor : 通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题
弊端: 无法控制依赖的版本、更新项目又可能出现依赖冲突,导致编译出错
Go Module
· 通过 go.mod 文件管理依赖包版本
· 通过 go get/go mod 指令工具管理依赖包
Go Module的最终目标:可以定义版本规则和管理项目依赖关系
依赖管理三要素
- 配置文件, 描述依赖 go.mod
- 中心仓库管理依赖库 Proxy
- 本地工具 go get/mod
依赖配置
go.mod
version
GOPATH 和 Go Verdor都是源码副本进行的依赖,没有具体的版本规则。
Go Module 为了更好的做版本管理,定义了版本规则,主要分为语义化版本,基于commit伪版本
语义化版本:
{MINOR}.${PATCH}
MAJOR :属于大版本,不同的MAJOR版本可以不兼容
MINOR :做一些函数的时候,需要在MAJOR下做到前后兼容
PATCH :一般做一些代码版本的修复
基于 commit 伪版本
vX.0.0-yyyymmddhhmmss-abcdefgh12345
版本前缀——时间戳——哈希码前缀
incompatilble
· 主版本2+模块会在模块路径增加 /vN 后缀
· 对于没有go.mod文件并且主版本2+的依赖,会 +incompatilble
依赖分发
回源
· 无法保证构建稳定性
· 无法保证依赖可用性
· 增加第三方压力
Proxy
Proxy是一个服务站点,会缓存原栈中的软件内容,软件版本也不会改变。Proxy可以保证软件的一个稳定性和可靠性。
从Proxy直接拉去依赖
变量 GOPROXY
GOPROXY="proxy1.cn, proxy2.cn ,direct"
服务站点URL列表,“direct”表示源站
测试
单元测试
单元测试优势:保证质量、提高效率
规则
· 所有测试文件以 _test.go结尾
· 测试定义**func TestXxx(testing.T)*
· 初始化逻辑放到 TestMain 中
m.Run() 代表跑package下的所有单元测试
例子
//...
func HelloTom() string {
return "Jack"
}
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
if output != expectOutput {
t.Errorf("Expected %s don't %s", expectOutput, output)
}
}
assert
assert包中包含了Equal、NotEqual等等,我们可以调用asset包来简化代码
import ("github.com/stretchr/testify/assert")
然后将上述if判断语句可以改成:
assert.Equal(t, expectOutput, output)
代码覆盖率
可以通过代码覆盖率来判断代码功能是否完备,代码是否含有BUG
func JudgePassLine(score int16) bool {
if score >= 60 {
return true
}
return false
}
func TestJudgePassLineTrue(t *testing.T) {
isPass := JudgePassLine(78)
assert.Equal(t, true, isPass)
}
以上的执行代码,代码覆盖率可以达到66.7% ,因为第一个函数只执行了return true的分支
当再加一个单元测试来执行第二条分支的时候,代码覆盖率可以达到100%
· 一般覆盖率: 50% ~ 60%, 较高覆盖率80%+
· 测试分支相互独立、全面覆盖
· 测试单元粒度足够小,函数单一职责
依赖
单元测试强依赖File、DB、Cache等等
Mock
外部依赖可以通过Mock来实现,保证单元测试的稳定性
可以理解为:用函数A替换函数B,B是原函数,A是打桩函数
func Patch(target, replacement interface{}) *PatchGuard {
t := refflect.ValueOf(target)
r := reflect.ValueOf(replacement)
patchValue(t, r)
return &PatchGuard{t, r}
}
func Unpatch(target interface{}) bool {
return unpatchValue(reflect.ValueOf(target))
}
targe是原函数
replacement是需要打桩的函数
Unpatch函数保证测试完成之后卸载掉桩
func TestProcessFirstLineWithMock(t *testing.T) {
monkey.Patch(ReadFirstLine, func() string {
return "line110"
})
defer monkey.Unpatch(ReadFirstLine)
line := ProcessFirstLine()
assert.Equal(t, "line000", line)
}
功能为寻找该文件中存不存在“line000“内容
完全不依赖本地的文件,可以在任何时候运行
基准测试
功能:测试一段程序运行时的性能和CPU的损耗
使用方法类似于单元测试
· *func BenchmarkXxx (b testing.B) {}
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)]
}
对上述代码进行基准测试
// 串行实现基准测试
func BenchmakeSelect(b *testing.B) {
InitServerIndex()
b.ResetTimer()
for i := 0; i < b.N; i++ {
Select()
}
}
//并行实现基准测试
func BenchmakeSelectParallel(b *testing.B) {
InitServerIndex()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Select()
}
})
}
因为InitServerIndex() 不属于我们测试函数损耗,所以要将它的时间抛掉
b.ResetTimer() :对定时器重置
并行做基准测试时,性能会进行劣化。因为前面Select函数中使用了rand函数。
rand函数为了保证全局的随机性和并发安全,持有了全局锁,在一定情况下就降低了并行的性能
Fastrand函数
在工程中使用随机函数非常频繁,但又因为rand函数对并发效率不是很高,所以出现了Fastrand函数
func FastSelect() int {
return ServerIndex[fastrand.Intn(10)]
}
fastrand牺牲了一些随机数列的一致性,但影响不大