Day2 Go语言工程进阶 | 青训营笔记

98 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 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依赖管理三要素

  1. 配置文件 go.mod
  2. 中心仓库管理依赖库
  3. 本地工具 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