这是我参与「第五届青训营 」伴学笔记创作活动的第 N 天
一 并发编程
1.1 Goroutine和Channel
goroutine是go的协程实现,在函数前面写上关键字go,就表示go程序会起一个协程去执行这个函数。
channel是管道,用于协程之间的通信。管道有自己的类型和空间。可以理解成一个队列。协程a可以往管道里写数据,而协程b可以从管道里拿数据。
管道是线程安全的,每次能有一个协程访问这个管道。下面是一个生产者消费者的例子,生产者依次生成0~9这几个数字,然后消费者消费这些数字并计算他们的平方交给用户。
首先我们用make来创建管道用来通信。make的第二个参数表示管道的大小,如果没有默认是0,也就是必须同步的读写。
这里我们用了两个管道,第一个管道src是生产者用来生产数字给消费者的。消费者通过src消费了数字后,计算他们的平方,然后通过第二个管道dest传给用户。
func main() {
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 v := range dest {
fmt.Println(v)
}
}
1.2 锁 sync.Mutex
sync.Mutex是go提供的互斥量,对互斥量加锁解锁可以实现对临界资源的保护,避免并发安全问题。在sync包中,sync.Mutex有两个方法,Lock和Unlock,这两个方法就是标准的互斥量用法,在进入临界区的时候加锁,退出的时候解锁。
协程如果想要进入临界区,必须先获得锁。如果已经有其他的协程获取锁,并进入了临界区。那么这个协程只能等锁释放。
下面是一个并发安全问题的例子,我们用5个协程对一个临界变量做自增,每个协程都自增2000次。如果不对x加锁,显然由于并发问题会导致实际上x值小于2000 * 5。
var x int
var lock sync.Mutex
func addWithLock() {
for i := 0; i < 2000; i++ {
lock.Lock()
x++
lock.Unlock()
}
}
func addWithoutLock() {
for i := 0; i < 2000; i++ {
x++
}
}
func main() {
x = 0
for i := 0; i < 5; i++ {
go addWithLock()
}
time.Sleep(time.Second)
fmt.Println(x)
x = 0
for i := 0; i < 5; i++ {
go addWithoutLock()
}
time.Sleep(time.Second)
fmt.Println(x)
}
1.3 WaitGroup
sync.WaitGroup主要是用来做协程之间的同步的。它适用于这样的场景,一个主协程有很多子协程。主协程希望等待所有的子协程的任务执行完了以后,再执行别的任务。这时候可以用WaitGroup实现这种需求。
WaitGroup的本质就是一个计数器,它会计数当前等待的协程数量。它有3个常用的方法Add,Done和Wait。
- Add(i int) 表示计数器增加i
- Done() 表示计数器减一
- Wait() 这个方法一般是主协程调用,用来等待其他的协程执行完。
func hello(i int) {
fmt.Println("hello goroutine,", i)
}
func HelloGoroutine() {
wg := sync.WaitGroup{}
wg.Add(5)
for i := 0; i < 5; i++ {
go func(i int) {
defer wg.Done()
hello(i)
}(i)
}
wg.Wait()
}
func main() {
HelloGoroutine()
}
二 依赖管理
gopath和govender是过时的东西就不细说了,下面说下go mod。
go mod依赖管理三要素
- 配置文件 go.mod
- 中心仓库管理依赖库
- 本地工具 go get/mod
go.mod文件里面有三个部分,第一行表示该依赖管理的基本单元。下面是使用的go的版本。最下面是依赖的其他库,放在了require里面。
modoule example/project/app
go 1.16
require(
example/lib1 v1.1.2
example/lib2 v0.1.0-20190725025543-bacd9c7ef1dd
)
require里的信息有两部分,一部分是库的路径,另一个部分是版本号。这里版本号又有两种,第一种是标准的版本号:{Major}.{Minor}.{Patch}。
major表示大版本,同一个大版本下面的库是兼容的。minor表示小版本,一般新增一些功能和函数,patch是小补丁。
第二种是基于commit伪版本号,这种版本号会在后面附上git提交的版本hash
go get和go mod是依赖管理的两个工具。go get能直接拉取需要的依赖
go get example.org/pkg
还可以在后面加上@v1.1.1来拉去指定版本的依赖
go mod是管理项目中的依赖,有几个常用的命令:
- go mod init 初始化项目,用go mod管理
- go mod download 下载依赖到缓存
- go mod tidy 下载没有的依赖,并且删除不需要的依赖
三 测试
3.1 go中的单元测试
所有的测试文件都以_test.go结尾
所有的测试函数都以Test开头
初始化逻辑放到TestMain中
代码覆盖率:指的是测试的时候测试了多少行代码,在实际应用中一般50%~60%,较高80%
下面是一个例子,这里判断结果与预期是否符合用了一个第三方库:github.com/stretchr/te…
func TestJudgePassLineTrue(t *testing.T) {
isPass := JudgePassLine(70)
if isPass != true {
t.Errorf("Expect %t, get %t", true, false)
}
}
func TestJudgePassLineFalse(t *testing.T) {
isPass := JudgePassLine(50)
if isPass != false {
t.Errorf("Expect %t, get %t", true, false)
}
}
3.2 Mock打桩
外部依赖用mock测试,这里介绍了monkey这个第三方库github.com/bouk/monkey
打桩意思就是说,需要测试的代码里面有一些库函数没有实现,这时候需要我们用一个桩函数替换来测试这些代码。桩函数是一些非常简单的函数,只是为了保持接口返回值什么的匹配而已。比如一个a+b的函数func1,库函数还没有实现这个函数,那么我们可以设置一个桩函数,接口与a+b一样,但是直接返回0