这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天。
Go语言并发编程
并发与协程
在Java语言中,更多地会强调进程、线程和线程池的概念,而在Go语言中,经常会使用比线程更轻量级的协程,线程与协程的对比如下所示,Go语言一次可以创建上万级别的协程。
- 协程:用户态,轻量级线程,栈KB级别。
- 线程:内核态,线程跑多个协程,栈MB级别。
Go将协程和并发简化到了仅需一个 go 关键字即可完成,而不像其他语言的协程一样及其繁琐复杂。
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
time.Sleep(time.Second)
}
主协程main是一个特殊的协程,如果主协程执行完毕,那么其他子协程也会停下手上的活,直接退出。这里的go协程有点类似于Java中的守护线程的意思,因此,当我们有多个子协程执行时,应该等待这些协程全部执行完毕后,再结束主协程。
WaitGroup
func HelloGoRoutine() {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func(j int) {
defer wg.Done()
hello(j)
}(i)
}
wg.Wait()
}
var wg sync.WaitGroup声明了一个名为wg的WaitGroup变量;- 调用
WaitGroup实例的Add方法,传入数字5; - 在匿名函数开头,延迟调用
WaitGroup实例的Done方法; - 在函数尾部调用
WaitGroup实例的Wait方法。
defer 关键字代表“在函数末尾执行”,被 defer 关键字标注的代码会以 先进后出 的顺序被移动到函数末尾执行。比如,我们打开了一个 IO 流,需要在函数结束时释放流(调用 Close 方法),这时,我们便可以直接在打开的流实例下直接填写,来确保流一定会在函数结束时释放,避免遗忘。
WaitGroup 其实内部维护了一个计数器,并以如下方式工作:
- 通过调用
Add方法,向WaitGroup的计数器添加指定值; - 通过调用
Wait方法阻塞当前协程,这会使得协程陷入无限的等待; - 通过调用
Done方法使WaitGroup内部的计数器 -1,直到计数器值为 0 时,先前被阻塞的协程便会被释放,继续执行接下来的代码或是直接结束运行。
因此,上述代码为 WaitGroup 的计数器 +5,随后阻塞主协程,当所有 5 个子协程纷纷调用 Done 方法后,主协程便会被释放,然后结束程序运行。
Channel
通道是协程之间用来传递数据的一个数据结构,通过传递一个指定类型的值来同步运行和通讯。操作符<-用于指定通道的方向,实现发送or接收。
通道分为有缓存的通道和无缓存的通道,无缓存 Channel 意味着,一个数据的发送必须等待另一端代码的接收,如果没有人接收发送的数据,那么发送端便会被永远阻塞。这意味着,Channel 内可以有最多N个数据未被接收方接收,此时,发送方可以直接发送数据而不必收到阻塞,如果超出缓冲区,则依旧会被阻塞。
在使用通道的过程中,可以通过使用 for range 的方式来从一个 Channel 中取出所有数据。
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)
}
}
Go依赖管理
Go Module
Go Module 通过项目路径中的 go.mod 文件来配置文件,描述依赖;然后,通过 go.sum 文件记录项目实际使用的依赖和版本。我们没有必要像 Java 的 Maven那样手动编辑配置文件指定依赖,Go 为我们提供了 go get 和 go mod 两条指令来方便的添加和移除项目中的依赖。
Go Module在GoLand 1.16以后默认开启。
依赖分发
依赖分发就是这个依赖要去哪里下载,如何下载的问题。实际上是用Proxy来缓存,保证了依赖的稳定性。通过指定 Proxy 服务器,优先从 Proxy 服务器拉取依赖,这不仅减轻了源站负担,也保证了依赖的可用性,避免依赖库开发者删库跑路。
go mod 指令
go mod 是一个命令行指令,可用于初始化项目和管理依赖,在项目目录执行它以为项目配置依赖:
go mod init,初始化项目,这将创建 go.mod 文件go mod download,下载模块到本地缓存go mod tidy,添加需要的依赖,删除不需要的依赖
单元测试
通常,有多种测试方法可以使用,例如回归测试,集成测试,单元测试,而单元测试(Unit Test) 是成本最低,覆盖率最高的测试方法。所谓单元测试,便是为代码的每一个模块,函数定制单独的测试,保证输入指定值后输出正常值。通过多个单元测试合并运作,我们便可得知项目的每一个细节都在正确运行,最终得知项目整体运作正常。
测试规则
- 所有测试文件以_test.go结尾
- func TestXxx(*testing.T):一个单元测试函数的函数名应当以
Test开头,并包含*testing.T形参 - 初始化逻辑放到TestMain中
我们也可以引入社区提供的依赖库来加快单元测试开发,诸如通过 testify/assert 库进行覆盖率测试,或通过 bouk/monkey 库对数据进行 Mock。
import(
"github.com/stretchr/testify/assert"
"testing"
)
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
assert.Equal(t, expectOutput, output)
}
func HelloTom() string {
return "Tom"
}
// 用函数A去替换函数B,B就是原函数,A就是打桩函数
func Patch(target, replacement interface{}) *PatchGuard {
// target就是原函数,replacement就是打桩函数
t := reflect.ValueOf(target)
r := reflect.ValueOf(replacement)
patchValue(t, r)
return &PatchGuard{t, r}
}
func Unpatch(target interface{}) bool {
// 保证了在测试结束之后需要把这个包卸载掉
return unpatchValue(reflect.ValueOf(target))
}
func TestProcessFirstLineWithMock(t *testing.T) {
monkey.Patch(ReadFirstLine, func() string {
return "line110"
})
defer monkey.Unpatch(ReadFirstLine)
line := ProcessFirstLine()
assert.Equal(t, "line000", line)
}
// 通过patch对ReadFirstLine进行打桩mock,默认返回line110,通过defer卸载mock
// 这样整个测试函数就摆脱了本地文件的束缚和依赖
基准测试
基准测试是指测试一段程序的性能及耗费CPU的程度;
在实际的项目开发中,经常会遇到代码性能瓶颈,为了定位问题,经常要对代码做性能分;
这时就用到了基准测试,其使用方法与单元测试类似。