这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天,今天学习了Go并发特性,依赖管理,测试等知识,下面是我的笔记
Go语言进阶与工程实践
1、Go并发特性
1.1、协程和Goroutine
- 协程的定义和作用
协程是比线程更加轻量级的存在,它就是一个可以在某个地方挂起的特殊函数,并且可以重新在挂起处继续运行。所以说,协程与进程、线程相比,不是一个维度的概念。
一个进程可以包含多个线程,一个线程也可以包含多个协程,也就是说,一个线程内可以有多个那样的特殊函数在运行。
一个线程内的多个协程的运行是串行的。如果有多核CPU的话,多个进程或一个进程内的多个线程是可以并行运行的,但是一个线程内的多个协程却绝对串行的,无论有多少个CPU(核)。因为协程虽然是一个特殊的函数,但仍然是一个函数。
一个线程内可以运行多个函数,但是这些函数都是串行运行的。当一个协程运行时,其他协程必须挂起。
进程、线程、协程
| 进程 | 线程 | 协程 | |
|---|---|---|---|
| 切换者 | os | os | user |
| 切换时机 | by os | by os | by user |
| 切换内容 | 刷新tlb,内核栈,context上下文 | 内核栈,上下文 | 硬件上下文 |
| 切换内容的保存 | 内核栈 | 内核栈 | 用户堆栈 |
| 切换过程 | 用户态-内核态-用户态 | 用户态-内核态-用户态 | 用户态(不会进入内核态) |
| 切换效率 | 低 | 略高 | 最高 |
协程使用场景:
io阻塞性,单核cpu,不适用与io密集型,需要并发性能的场景,因为协程本质上是串行化的。
goroutine就是基于协程实现的
- goroutine的使用,下面通过一段代码来展示
func DelayPrint() {
for i := 1; i <= 4; i++ {
time.Sleep(250 * time.Millisecond)
fmt.Println(i)
}
}
func HelloWorld() {
fmt.Println("Hello world goroutine")
}
func main() {
go DelayPrint() // 开启第一个goroutine
go HelloWorld() // 开启第二个goroutine
time.Sleep(2*time.Second)
fmt.Println("main function")
}
main函数执行不关心goroutine是否结束
,且会强制退出所有未完成的goroutine函数
1.2、channel
-
Go提倡通过通信共享内存而不是共享内存来通信
-
Go语言使用channel来进行通信
-
channel的定义方法如下
var ch chan int // 声明一个传递int类型的channel ch := make(chan int) // 使用内置函数make()定义一个channel,显然内部参数是channel的存入元素的类型。
//=====================================
ch <- value // 将一个数据value写入至channel,这会导致阻塞,直到有其他goroutine从这个channel中读取数据 value := <-ch // 从channel中读取数据,如果channel之前没有写入数据,也会导致阻塞,直到channel中被写入数据为止
//=======================================
close(ch) // 关闭channel 使用<-符号负责传输,无缓冲默认会阻塞的
//=======================================
ch := make(chan string, 3) // 创建了缓冲区为3的通道
带缓冲区的channel,可以解决生产消费者模式的效率问题。
make为初始化操作,注意如果在main函数外面则必须用var echo chan string的方法来声明channel,但是使用之前必须make一下。 //========= len(ch) // 长度计算 cap(ch) // 容量计算
1.3、sync
sync设计了众多线程安全的锁,这里介绍了
-
sync.Mutex
-
sync.WaitGroup
Mutex一种互斥锁。互斥体的零值是一个解锁的互斥体。
互斥体在第一次使用后不得复制。在Go内存模型的术语中,对于任何n < m的情况,第n次调用Unlock“先于”第m次调用lock。成功调用TryLock等同于调用Lock。对TryLock的调用失败根本不会建立任何“之前同步”关系。
func (m *Mutex) Lock()如果锁已经被使用,调用的goroutine将阻塞,直到互斥锁可用。
func (m *Mutex) TryLock() boolTryLock尝试锁定m并报告是否成功。
func (m *Mutex) Unlock()解锁m。如果m在进入解锁状态时未被锁定,则出现运行时错误。
`
waitgroup类型是计数器,有三个主要方法:
func (wg *WaitGroup) Add(delta int)
Add向WaitGroup计数器添加增量,增量可能为负值。如果计数器变为零,所有阻塞等待的goroutines将被释放。如果计数器为负,则增加panic。
Done将WaitGroup计数器减1。
func (wg *WaitGroup) Wait()
等待块,直到WaitGroup计数器为零。
//将Done函数放在协程的开头defer一下,能保证减一的操作,代码如下。
go func addWithLock() {
defer ss.Done()
for i := 0; i < 2000; i++ {
lock.Lock()
x += 1
lock.Unlock()
}
}()
2、Go的依赖管理
2.1、依赖管理演进
Gopath-->Govendor-->Go module
2.2、Gopath的缺陷
项目代码直接依赖于src包,只能在src下建项目。
缺陷
- 同一个包依赖冲突
- 版本控制困难
- 只能在src包下编程,强迫症难受,不灵活。
2.3、Govendor
项目目录下增加vendor文件,所有依赖包副本形式放在ProjectRoot/vendor中,
vendor=>GOPATH,解决了多个项目需要同一个包的依赖冲突问题。
- 但是存在不同版本的依赖冲突问题
2.4、Go Module
-
go.mod文件管理依赖版本
-
中心仓库管理依赖库Proxy
-
本地工具 go get/mod
类似于java中的maven。
3、测试
3.1、Go语言单元测试及基准测试
Go语言配置了单元测试相关的库,/test,编写test测试的主要规则有:
-
所有测试文件以_test.go结尾
-
func TestXxx(*testing.T)
-
初始化逻辑放到TestMain中
func Sum(arr []int) int { res := 0 for _, value := range arr { res += value } return res } func TestSum(t *testing.T) { encode := Sum([]int{1, 23, 3}) expectedVal := 25 if encode != expectedVal { t.Error("这是一个错误") } } -------------------------------- === RUN TestSum main_test.go:9: 这是一个错误 --- FAIL: TestSum (0.00s) FAIL
func BenchmarkXxx(*testing.B)
被视为基准,当提供了-bench标志时,由“go test”命令执行。基准是按顺序运行的。
一个示例基准函数如下所示:
func BenchmarkRandInt(b *testing.B) {
for i := 0; i < b.N; i++ {
rand.Int()
}
}
基准函数必须运行目标代码b.N次。在基准执行期间,b.N被调整,直到基准函数持续足够长的时间来可靠地计时。输出
BenchmarkRandInt-8 68453040 17.8 ns/op
意味着该循环以每循环17.8 ns的速度运行了68453040次。
如果基准测试在运行前需要一些昂贵的设置,可以重置计时器:
func BenchmarkBigLen(b *testing.B) {
big := NewBig()
b.ResetTimer()
for i := 0; i < b.N; i++ {
big.Len()
}
}
3.2、开源测试包assert
go get "github.com/stretchr/testify/assert"
通过以上命令获取该包进行测试,assert包下封装了一些常用的测试手段。
assert.Equal(t, encode, expectedVal, "这是一个错误")
3.3、单元测试的覆盖率
go test _test,go --cover 显示覆盖率
- 一般覆盖率:50%-80%
- 测试分支相互独立、全面覆盖。
- 测试单元粒度足够小,函数单一职责。
3.4、Mock测试
mock测试运用的场景:
-
需要将当前被测单元和其依赖模块独立开来,构造一个独立的测试环境,不关注被测单元的依赖对象,只关注被测单元的功能逻辑。
-
被测单元依赖的模块尚未开发完成,而被测单元需要依赖模块的返回值进行后续处理。
-
前后端项目中,后端接口开发完成之前,接口联调;
-
依赖的上游项目的接口尚未开发完成,需要接口联调测试;
-----比如service层的代码中,包含对Dao层的调用,但是,DAO层代码尚未实现
- 被测单元依赖的对象较难模拟或者构造比较复杂。