一、Go并发编程
1、并发与并行的概念
并发的实质是一个物理CPU(也可以是多个物理CPU)在若干个程序之间多路复用,并发性是对有限物理资源强制行使多用户共享以提高效率。
并行指两个或两个以上事件或活动在同一时刻发生,在多道程序环境下,并行使多个程序同一时刻可在不同CPU上同时执行。
并发是在同一个cpu上同时(不是真正的同时,而是看来是同时,因为CPU要在多个程序之间切换)运行多个程序。
并行是每一个CPU运行一个程序。
并发是指一个处理器同时处理多个任务。
并行是指多个处理器或者是多核的处理器同时处理多个不同的任务。
并发是逻辑上的同时发生,而并行是物理上的同时发生。
2、协程Goroutine
协程:用户态,轻量级线程,栈KB级别。
线程:内核态,线程跑多个协程,栈MB级别。
协程的创建和调度由Go语言本身完成,这也是Go语言适合高并发场景的原因所在。
// 协程的使用
package main
import (
"fmt"
"time"
)
func hello(i int) {
println("hello goroutine : " + fmt.Sprint(i))
}
//快速且无序打印
func HelloGoRoutine() {
for i := 0; i < 5; i++ {
// 创建协程,在调用函数时,前面加上go,就为一个函数创建了一个协程
go func(j int) {
hello(j)
}(i)
}
// time.Sleep()的作用是:保证了子协程在执行完之前,主协程不退出。
time.Sleep(time.Second)
}
func main() {
HelloGoRoutine()
}
/*执行结果
hello goroutine : 4
hello goroutine : 0
hello goroutine : 2
hello goroutine : 1
hello goroutine : 3
*/
协程之间的通信提倡通过通信共享内存而不是通过共享内存而实现通信。
3、通道Channel
Channel是一种引用类型,通过make关键字进行创建。
make(chan 元素类型,[缓冲大小])
根据是否有缓冲区的大小,channel又可分为无缓冲通道和有缓冲通道。
- 无缓冲通道:make(chan int)
- 有缓冲通道:make(chan int,2)
无缓冲通道又称为同步通道,解决同步问题的一个方式就是使用带有缓冲区的有缓冲通道,通道的容量代表通道中能存放的元素。
package main
import (
"fmt"
)
func CalSquare() {
src := make(chan int) // 无缓冲通道
dest := make(chan int, 3) // 有缓冲通道
// A子协程发送0~9数字
go func() {
defer close(src) // 延迟的资源关闭
for i := 0; i < 10; i++ {
src <- i // 生产的数字发送到src这个channel中
}
}()
// B子协程dest计算输入数字的平方
go func() {
defer close(dest)
// 通过 range 关键字来实现遍历src中的数据,实现A协程和B协程的通信
for i := range src {
dest <- i * i // 将数字的平方发送到src这个channel中
}
}()
// 主协程输出最后的平方数
for i := range dest {
// 因为主协程可能会有更多的复杂操作,比较耗时,所以用带缓冲的通道可以避免问题
println(i)
}
}
func main() {
CalSquare()
}
4、并发安全Lock
4.1、互斥锁
对变量执行2000次+1操作,5个协程并发执行,并对比两种情况即加锁和不加锁的区别。
package main
import (
"fmt"
"sync"
"time"
)
// 除了使用channel实现同步之外,还可以使用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 // 直接进行+1操作
}
}
func Add() {
x = 0
for i := 0; i < 5; i++ {
go addWithoutLock() //开启5个协程,会输出未知结果
}
time.Sleep(time.Second)
println("WithoutLock :", x)
x = 0
for i := 0; i < 5; i++ {
go addWithLock() //开启5个协程,加锁会输出预期值10000
}
time.Sleep(time.Second)
println("WithLock :", x)
}
func main() {
Add()
}
/*执行结果
WithoutLock : 7286
WithLock : 10000
*/
并发安全问题会一定概率的引起错误的出现。
4.2、WaitGroup
由于不知道子协程的执行时间,所以无法设置一个精确的sleep时间,Go语言中使用WaitGroup实现并发任务的同步。
package main
import (
"fmt"
"sync"
)
func hello(i int) {
println("hello WaitGroup :", fmt.Sprint(i))
}
func ManyGoWait() {
var wg sync.WaitGroup
wg.Add(5) // 开启5个协程
for i := 0; i < 5; i++ {
go func(j int) {
defer wg.Done() // 表明子协程任务执行结束
hello(j)
}(i)
}
wg.Wait() // 阻塞
}
func main() {
ManyGoWait()
}
/*执行结果
hello goroutine : 4
hello goroutine : 3
hello goroutine : 1
hello goroutine : 0
hello goroutine : 2
*/
二、依赖管理
依赖其实是各种开发包,对于复杂的工程项目不可能基于标准库从0到1编码搭建,而应该站在巨人的肩膀上,更多的关注已有逻辑的实现,对各种包进行依赖并管理。
1、Go依赖管理的演进
GOPATH
--> Go Vender
--> Go Module
1.1、GOPATH
GOPATH是环境变量,含有三个部分:
- bin:项目编译的二进制文件
- pkg:项目编译的中间产物,加速编译
- src:项目源码,项目代码直接依赖src下的代码,go get下载最新版本的包到src目录下
弊端:A和B依赖于某一package的不同版本,会出现问题,所以GOPATH无法实现package的多版本控制。
1.2、Go Vender
项目目录下增加vender文件,在vender存放了所有依赖包的副本。
依赖寻址方式:vender --> GOPATH,项目的依赖会优先在vender中寻找,找不到则在GOPATH中寻找。
通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题。
弊端:无法控制依赖的版本;更新项目的时候又可能导致依赖冲突,导致编译出错。
1.3、Go Module
- 通过
go.mod
文件管理依赖包版本 - 通过
go get/go mod
指令工具管理依赖包
终极目标:定义版本规则和管理项目依赖关系
2、依赖管理三要素
- 配置文件,描述依赖
go.mod
- 中心仓库管理依赖库
Proxy
- 本地工具
go get/mod
3、依赖配置
indirect
对于没有直接导入的标识模块会表示为非直接依赖,并在go.mod
中加上通过// indirect
进行标识。
(A --> B --> C)
中A --> B
为直接依赖,A --> C
为间接依赖。
incompatible
- 主版本2+模块会在模块路径增加
/vN
后缀 - 对于没有
go.mod
文件并且主版本2+的依赖,会加上+incompatible
,表示可能会存在一些不兼容的代码逻辑
4、依赖分发
GOPROXY
GOPROXY=“https://proxy1.cn,https://proxy2.cn,direct”
服务站点URL列表,direct表示源站。
查找依赖的逻辑:Proxy 1 --> Proxy 2 --> Direct
三、测试
测试是避免事故的最后一道屏障。
1、单元测试
1.1、测试规则
- 所有测试文件以_test.go结尾,方便区分
- func TestXxx(*testing.T)
- 初始化逻辑放到TestMain中
1.2、assert
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"
}
1.3、覆盖率
覆盖率:
- 衡量代码是否经过了足够的测试
- 评价项目的测试水准
- 评估项目是否达到了高水准测试等级
func JudgePassLine(score int16) bool {
if score >= 60 {
return true
}
return false
}
func TestJudgePassLineTrue(t *testing.T) {
isPass := JudgeePassLine(70)
assert.Equal(t, true, isPass)
}
func TestJudgePassLineFalse(t *testing.T) {
isPass := JudgeePassLine(50)
assert.Equal(t, false, isPass)
}
tips:
- 一般覆盖率:50%~60%,较高覆盖率:80%+。
- 测试分支相互独立、全面覆盖。
- 测试单元粒度足够小,函数单一职责。
1.4、依赖
稳定:单元测试是相互隔离的,测试在任何时间任何函数能独立地运行。
幂等:重复运行一个测试时,结果与之前相同。
外部依赖 => 稳定&幂等
1.5、文件处理
func ReadFirstLine() string {
open, err := os.Open("log") // 打开一个文件
defer open.Close()
if err != nil {
return ""
}
scanner := bufio.NewScanner(open) // 对每行进行遍历
for scanner.Scan() {
return scanner.Text()
}
return ""
}
func ProcessFirstLine() string {
line := ReadFirstLine()
destLine := strings.ReplaceAll(line, "11", "00") // 替换11为00
return destLine
}
func TestProcessFirstLine(t *testing.T) { // 执行单元测试
firstLine := ProcessFirstLine()
assert.Equal(t, "line00", firstLine)
}
2、Mock测试
monkey: https://github.com/bouk/monkey 这是一个开源的mock包,可以对method或者实例的方法进行mock。
快速Mock函数:
- 为一个函数打桩
- 为一个方法打桩
// 用函数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打桩测试
3、基准测试
基准测试是指测试一段程序的性能及耗费CPU的程度。在实际的项目开发中,经常会遇到代码性能瓶颈的问题,为了定位问题,需要对代码做性能分析,这时就用到了基准测试,使用方法与单元测试类似。
- 优化代码,需要对当前代码分析
- 内置的测试框架提供了基准测试的能力