这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天
重点概览
- 从并发编程的角度了解高性能的本质
- 了解go语言依赖管理的演进路线
- 提升质量意识
详细介绍
go:为并发而生
并发:多线程程序在一个核的cpu上运行
并行:多线程程序在多个核的cpu上运行
并行:实现并发的一种手段
go实现了一个并发性能极高的调度模型,通过高效调度最大限度的利用计算资源,从而充分发挥多核计算机的优势
协程Goroutine
- 协程:用户态,轻量级线程,栈KB级别
- 线程:内核态,线程跑多个协程,栈MB级别
Go语言一次可以创建上万级别的协程。通过go关键字来开启goroutine;
goroutine是一个轻量级线程,其调度是由Golang运行时进行管理的
//实现快速打印
func hello(i int) {
println("hello gorountine: " + fmt.Sprint(i))
}
func HelloGoRoutine() {
for i := 0; i < 5; i++ {
go func(j int) {
hello(j)
}(i)//匿名函数
}
// 保证子协程完成前, 主线程不退出
time.Sleep(time.Second)
}
CSP(Communicating Sequential Processes)
通信顺序进程:
提倡通过通信共享内存而不是通过共享内存而实现通信
Channel
make(chan 元素类型, [缓冲大小])
- 无缓冲通道(同步通道) make(chan int)
- 有缓冲通道 make(chan int, 2)
通道是用来传递数据的一个数据结构,可以用于两个goroutine之间,通过传递一个指定类型的值来同步运行和通讯。
func CalSquare() {
// src实现协程A和B的通信
src := make(chan int)
// dest实现协程B和主协程的通信
// 这里用有缓冲通道, 是考虑到主协程复杂操作速度较慢,借缓冲协调速度不均衡
dest := make(chan int, 3)
go func() { // 协程A: 子协程发送0-9数字
defer close(src)
for i := 0; i < 10; i++ {
src <- i
}
}()
go func() { // 协程B: 计算输入数字的平方
defer close(dest)
for i := range src {
dest <- i * i
}
}()
// 主协程输入最后的平方数
for i := range dest {
// 复杂操作
println(i)
}
}
并发安全Lock
Mutex互斥锁来实现同步
同步的含义:两个或多个协程之间互相等待,而不是顺次执行。
var (
x int64
lock sync.Mutex
)
func addWithLock() {
for i := 0; i < 2000; i++ {
lock.Lock()
x += 1
lock.Unlock()
}
}
func addWithoutLock() {
for i := 0; i < 2000; i++ {
x += 1
}
}
func main() {
x = 0
for i := 0; i < 50; i++ {
// 这是未定义行为, 应避免
go addWithoutLock()
}
time.Sleep(time.Second)
println("WithoutLock:", x)
x = 0
for i := 0; i < 50; i++ {
go addWithLock()
}
time.Sleep(time.Second)
println("WithLock:", x)
}
WaitGroup实现同步:
package main
import (
"fmt"
"sync"
)
func HelloPrint(i int) {
fmt.Println("Hello WaitGroup :", i)
}
func ManyGoWait() {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func(j int) {
defer wg.Done()
HelloPrint(j)
}(i)
}
wg.Wait()
}
func main() {
ManyGoWait()
}
依赖管理
依赖指各种开发包,我们在开发项目中,需要学会站在巨人的肩膀上,也就是利用已经封装好的、经过验证的开发组件或工具来提升自己的研发效率。
对于hello world以及类似的单体函数只需要依赖原生SDK,而实际工程会相对复杂,我们不可能基于标准库0~1编码搭建。
在实际开发中我们更多关注业务逻辑的实现,而其他的涉及框架、日志、driver、以及collection等一系列依赖都会通过sdk的方式引入,这样对依赖包的管理就显得尤为重要。
发展历程
GOPATH
是一个环境变量,其中有三个部分:
- bin:项目编译的二进制文件
- pkg:项目编译的中间产物,加速编译
- src:项目源码,项目代码直接依赖src下的代码
go get下载最新版本的包到src目录下
存在的弊端:无法实现package的多版本控制
Go Vender
项目目录下增加vender文件,所有依赖包副本形式放在$ProjectRoot/vender
依赖寻址方式:vender -> GOPATH
通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题。
存在的弊端:更新项目的时候可能导致编译错误的冲突;无法控制依赖的版本。
Go Module(1.16以后默认开启)
- 通过
go.mod文件管理依赖包版本 - 通过
go get/go mod指令工具管理依赖包
终极目标:定义版本规则和管理项目依赖关系
依赖管理三要素
- 配置文件,描述以来——
go.mod - 中心仓库管理依赖库——
Proxy - 本地工具——
go get/mod
测试
单元测试
单元测试主要包括:输入,测试单元,输出,以及校对;
单元的概念比较广,包括接口,函数,模块等;用最后校对来保证代码的功能与我们的预期相符;
单元测试一方面可以保证质量,在整体覆盖率足够的情况下,一定程度上段保证了新功能本身的正确性,又未破坏原有代码的正确性。
另一方面可以提升效率,在代码有bug的情况下,通过编写单测,可以在一个较短周期内定位和修复问题。
单元测试规则
- 所有测试文件以_test.go结尾(这样在使用
go build进行构建时,测试代码才会被排除在外 - 测试函数格式:func TestXxx(*testing.T)
- 初始化逻辑放到TestMain中
Mock测试
mock 测试就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法。这个虚拟的对象就是mock对象。mock对象就是真实对象在调试期间的代替品。
因为我们实际编写程序都不会是一个简单类,而是有着复杂依赖关系的类,Mock 对象让我们在不依赖具体对象的情况下完成测试。
monkey: github.com/bouk/monkey 这是一个开源的mock测试库
快速Mock函数:
- 为一个函数打桩
- 为一个方法打桩
基准测试
基准测试是指测试一段程序的性能及耗费CPU的程度;
在实际的项目开发中,经常会遇到代码性能瓶颈,为了定位问题,经常要对代码做性能分;
这时就用到了基准测试,其使用方法与单元测试类似。
- 优化代码,需要对当前代码分析
- 内置的测试框架提供了基准测试的能力