这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记
如果有错误和其他意见,麻烦留言指正💖
1 .并发编程
关于 Go 并发之前整理过一篇随笔《初见Go-Go并发之解决竞争状态》
1.1 并发和并行
并行:让不同的代码片段同时在不同的物理处理器上执行,关键是同时做很多事情。
并发:同时管理很多事情,这些事情可能只做了一半就被暂停去做别的事情了。
1.2 Goroutine
线程:内核态,创建、切换、停止都是很重的系统操作,比较消耗资源;线程跑多个协程;栈MB级别
协程:用户态;轻量级线程;栈KB级别
1.3 协程通信
go 提倡通过通信来共享内存,而非通过共享内存来实现通信,go也保留了通过共享内存来实现通信的方法
atomic/mutex
通过共享内存来实现通信,Java表示这个我熟
- atomic 包提供了五类原子操作
- SwapT (交换)
- CompareAndSwapT (CAS)
- AddT (增加|减少)
- LoadT (读取)
- StoreT (写入)
- syn 包提供了互斥锁(mutex),用于在代码上创建一个临界区,保证同一时间只有一个 goroutine 可以 执行这个临界区代码。
Channel
通过通信来共享内存
原子函数和互斥锁都能工作,但是依靠它们都不会让编写并发程序变得更简单,更不容易出错,或者更有趣。
在 Go 语言里,你不仅可以使用原子函数和互斥锁来保证对共享资源的安全访 问以及消除竞争状态,还可以使用通道,通过发送和接收需要共享的资源,在 goroutine 之间做同步。这也是go推荐的做法。
使用 make 创建通道
// 无缓冲的整型通道
unbuffered := make(chan int)
// 有缓冲的字符串通道
buffered := make(chan string, 10)
WaitGroup
- Add() 开启协程+1
- Done() 执行结束-1
- Wait() 主协程阻塞直到计数器为0
2.依赖管理
我们在学Java的时候,从最初导入jar包,到后来直接用 maven 来自动引入第三方依赖。体会到了依赖版本工具的强大和方便。
Go 语言中的依赖版本工具经过了一个漫长的发展过程
2.1 Go依赖管理演进
GOPATH
版本: Go 1.11 之前
- 环境变量 $GOPATH
- 项目代码直接依赖 src 下的代码
- go get 下载最新版本的包到 src 目录下
// 目录结构
|
|--bin 项目编译的二进制文件
|--pkg 项目编译的中间产物,加速编译
|--src 项目源码
缺点: 这种模式下,go get没有版本管理的概念,无法处理依赖不同版本的问题,因为同一个依赖都存在同一个路径下面。
GO Vendor
Go官方还未推出Go Modules的时候,go有各式各样的依赖管理工具,go Vendor是其中之一
- 项目目录下增加了 vendor 文件,所有依赖包副本形式放在 $ProjectRoot/vedor
- 依赖寻址方式: vendor => GOPATH
通过每个项目引入一份依赖的副本,解决了多个项目需要同一个 package 依赖的冲突问题。
缺点:
- 无法控制依赖的版本
- 更新项目又可能出现依赖冲突,导致编译出错
Go Module
版本: Go1.11 开始推出Go Modules ,Go1.13开始不再推荐使用GOPATH。
- 通过 go.mod 文件管理依赖包版本
- 通过 go get/go mod 指令工具管理依赖包
构建时选择最低的兼容版本
2.2 依赖管理三要素:
- 配置文件,描述依赖:go.mod
- 中心仓库管理依赖库:Proxy
- 本地工具:go get/mod
go.mod
-
module path: 第一行是module path, 一般采用仓库+module name的方式定义。这样我们获取一个module的时候,就可以到它的仓库中去查询,或者让go proxy到仓库中去查询。
-
go directive:第二行是go directive。格式是
go 1.xx,它并不是指你当前使用的Go版本,而是指名你的代码所需要的Go的最低版本。 -
require: require段中列出了项目所需要的各个依赖库以及它们的版本
- 语义化版本格式: 主版本号.次版本号.修订号
- 伪版本号: vX.0.0-yyyymmddhhmmss-abcdefabcdef,因为依赖库没有发布版本,而go module需要指定这个库的一个确定的版本,所以才创建的这样一个伪版本号
- indirect注释: 间接的使用了这个库,但是又没有被列到某个go.mod中
- incompatible:有些库的版major版本已经大于等于2了,但是他们的module path中依然没有添加v2、v3这样的后缀。虽然可以引用,但是实际它们是不符合规范的。
Proxy
GOPROXY 是Go Modules的代理,可以通过镜像站点快速拉取,可以设置多个代理。例如
GOPROXY="https://proxy.golang.org,direct"
go get/mod
在控制台中输入go mod可以查看命令提示
The commands are:
download download modules to local cache 下载模块到本地缓存
edit edit go.mod from tools or scripts
graph print module requirement graph
init initialize new module in current directory 初始化,创建go.mod文件
tidy add missing and remove unused modules 增加需要的依赖,删除不需要的依赖
vendor make vendored copy of dependencies
verify verify dependencies have expected content
why explain why packages or modules are needed
可以在任意路径下执行go get
- 用于从远程代码仓库上下载并安装代码包
- 支持的代码版本控制系统有: Git , Mercurial(hg),SVN, Bazaar
- 指定的代码包会被下载到
$GOPATH中包含的第一个工作区的src目录中,然后再安装
3.测试
关于 Go 测试之前也整理过一篇随笔《初见Go-Go测试》
3.1 单元测试
规则
- 所有测试文件以_test.go 结尾
- 测试函数名必须以 Test 开头
- 初始化逻辑放到 TestMain 中
func TestMain(m *testing.M) {
// 测试前 : 数据装载、配置初始化等前置工作
code := m.Run()
// 测试后 : 释放资源等收尾工作
os.Exit(code)
}
代码覆盖率
作用:
- 衡量代码是否经过了足够的测试
- 评价项目的测试水准
- 评估项目是否达到了高水准测试等级
运行测试:
执行go test xx_test.go xx.go -cover获得 xx.go 的测试文件代码覆盖率摘要。
ok command-line-arguments 0.364s coverage: 92.9% of statements
一般情况下,代码覆盖率很难达到100% ,代码一般覆盖率要达到50%~60%,较高覆盖率为80%以上
提高代码覆盖率:
- 测试分支相互独立,全面覆盖
- 测试单元粒度足够小,函数单一职责
3.2 Mock测试
作用
让测试函数不依赖本地环境。比如一个函数通过请求远程库来获取数据,但是这时候远程库还未实现。又或者一个函数通过读取本地文件来修改,如果文件变动就会影响测试结果。
bou.ke/monkey 库简单介绍
monkey 是一个Go单元测试中十分常用的打桩工具,它在运行时通过汇编语言重写可执行文件,将目标函数或方法的实现跳转到桩实现,其原理类似于热补丁。
另外使用时要注意:
- monkey不支持内联函数,在测试的时候需要通过命令行参数
-gcflags=-l关闭Go语言的内联优化,否则可能无法成功。 - monkey不是线程安全的,所以不要把它用到并发的单元测试中。
Patch / Unpatch 方法
Patch接收两个参数 target 和 replacement
target 可以是函数或方法,replacement 是打桩函数。
// Patch replaces a function with another
func Patch(target, replacement interface{}) *PatchGuard {
t := reflect.ValueOf(target)
r := reflect.ValueOf(replacement)
patchValue(t, r)
return &PatchGuard{t, r}
}
Unpatch用于在测试结束后卸载桩
// Unpatch removes any monkey patches on target
// returns whether target was patched in the first place
func Unpatch(target interface{}) bool {
return unpatchValue(reflect.ValueOf(target))
}
简单使用
-
安装mock 库 monkey
go get bou.ke/monkey -
编写如下测试代码
import ( "fmt" "testing" "bou.ke/monkey" ) func A() string { return "A" } func TestA(t *testing.T) { monkey.Patch(A, func() string { return "B" }) defer monkey.Unpatch(A()) fmt.Println(A()) // "B" } -
在控制台执行
go test -gcflags=-l,输出"B" ,函数A() 被替换掉了
3.3 基准测试
基准测试是测量一个程序在固定工作负载下的性能。和普通测试不同的是,默认情况下不运行任何基准测试。我们需要通过-bench命令行标志参数手工指定要运行的基准测试函数。
- 每个测试函数必须导入testing包。
- 基准测试函数名必须以 Benchmark 开头
*testing.B类型参数除了提供和*testing.T类似的方法,还有额外一些和性能测量相关的方法。它还提供了一个整数N,用于指定操作执行的循环次数。
func BenchmarkBruteForce(b *testing.B) {
for i := 0; i < b.N; i++ {
bruteForce("hello world", "world")
}
}
在Goland中直接运行测试测试结果如下
goos: windows
goarch: amd64
pkg: MyUtils/stringMatch/bruteForce
cpu: Intel(R) Core(TM) i5-9300H CPU @ 2.40GHz
BenchmarkBruteForce
BenchmarkBruteForce-8 53356231 22.45 ns/op
PASS
参考资料
《Go语言实战》By William Kennedy
《一起弄明白go的依赖管理 Go Modues/ GOPATH》
\