一.并发编程
Go可以充分发挥多核优势,高效运行
1.协程
go协程有两种使用方式,一是go + 方法,二是go func(){}(),第一个括号,传入的参数及类型,第二个括号写给第一个括号传入的内容。
go中主进程有多个子协程的话,需要阻塞主进程,等待子协程全部执行完毕之后再结束主程序
用到
// 等待子协程执行完之后,主程序才会结束
time.Sleep(time.Second)
一个简单的子协程输出案例
package main
import (
"fmt"
"time"
)
func hello(i int) {
fmt.Println("hello goroutine:" + fmt.Sprint(i))
}
func HelloGoRoutine() {
for i := 0; i < 6; i++ {
//go 中协程的使用有两种方式,一是 go + 方法,二是go func() {} ()
go func(j int) {
hello(j)
}(i) // 用于给func 传参,传参给j
}
// 等待子协程执行完之后,主程序才会结束
time.Sleep(time.Second)
}
func main() {
HelloGoRoutine()
}
运行结果
hello goroutine:2
hello goroutine:5
hello goroutine:4
hello goroutine:1
hello goroutine:0
hello goroutine:3
2.Channel(通道)
channel用于子协程之间的通信,go中是提倡通过通信来共享内存,而不是通过共享内存来通信
创建通道
make(chan 元素类型,[缓冲大小])
无缓冲通道-----make(chan int)
有缓冲通道-----make(chan int, 2)
缓冲通道类似于,快递柜,快递送到的时候你不能及时领取,会在快递柜临时缓冲存放
实例
A子协程发送0~9数字
B子协程计算输入数字的平方
主协程输出最后的平方数
先创建通道一src:=make(chan int)用于子协程A与子协程B之间的通信,子协程A向src中传入数据,用src<-i的方式,用一个类似的箭头去指向通道
再创建通道二dest:=make(chan int, 2),dest是一个有缓冲通道,子协程A与B之间的关系类似于生产消费,生产的速度是要大于消费的速度的,所以需要给”消费“---子协程B加上缓冲大小,以免出错。
最后重要的是,需要defer close(src/dest)延时关闭两个通道,如果不关闭的话,子协程会一直等待信息的输入(死锁),就会有报错
如图是子协程Bdest通道没有关闭的情况下的报错
用for i := range sre/dest 可以遍历通道中的数据
以下是完整代码以及运行结果
package main
import (
"fmt"
"time"
)
func CalSquare() {
// 创建通道
src := make(chan int)
dest := make(chan int, 3)
// 子协程1 用于发送0~9的数字
go func() {
// 延时关闭通道
defer close(src)
for i := 0; i < 10; i++ {
// 向src通道中传入信息
src <- i
}
}()
// 子协程2 用于计算输入数字的平方
go func() {
defer close(dest)
for i := range src {
dest <- i * i
}
}()
for j := range dest {
fmt.Println(j)
}
}
func main() {
CalSquare()
}
运行结果
0
1
4
9
16
25
36
49
64
81
3.并发安全lock
声明全局变量x和锁lock,主程序中先执行5个子协程,每个子协程x++2000次,给每个子协程x++时上锁,等待x++完成之后再解锁,再执行5个子协程,每个子协程同样x++2000次,但是不上锁,这样的得出的结果的差异就是,上锁的x等于1000,没上锁的x在0~10000之间飘忽不定,每次运行的结果都不一样。
在上锁与不上锁的两个子协程中加上time.Sleep(time.Second)同样,等子协程执行完成后,主程序才会继续往下执行。
代码以及运行结果如下
package main
import (
"fmt"
"sync"
"time"
)
var (
x int64
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 Add() {
x = 0
for i := 0; i < 5; i++ {
go addWithLock()
}
// 等待前5个子协程执行完毕再往下执行
time.Sleep(time.Second)
fmt.Println("WithLock:", x)
x = 0
for i := 0; i < 5; i++ {
go addWithoutLock()
}
time.Sleep(time.Second)
fmt.Println("WithoutLock:", x)
}
func main() {
Add()
}
运行结果
WithLock: 10000
WithoutLock: 7557
4.WaitGroup
其实就是类似于python中的线程队列,一个子协程开始计数器加一,一个子协程结束计数器减一,直到计数器等于0才会解阻塞,运行之后的程序,就是优雅版的time.Sleep(time.Second)
这里注意,执行wg.Done 时需要延时执行,不然会出现,只输出了一个结果或者不会输出结果就结束了的情况,因为,计数器减一之后,wg.Wait可以立即判断到计数器为0了就解阻塞了。
func HelloGoRoutine() {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 6; i++ {
//go 中协程的使用有两种方式,一是 go + 方法,二是go func() {} ()
go func(j int) {
defer wg.Done()
hello(j)
}(i) // 用于给func 传参,传参给j
}
wg.Wait()
// 等待子协程执行完之后,主程序才会结束
//time.Sleep(time.Second)
}
运行结果
hello goroutine:0
hello goroutine:1
hello goroutine:4
hello goroutine:2
hello goroutine:5
二. 依赖管理
依赖管理https://juejin.cn/post/7189155594840834103
依赖管理的发展历史
GOPATH
是一个环境变量,其中有三个部分:
- bin:项目编译的二进制文件
- pkg:项目编译的中间产物,加速编译
- src:项目源码,项目代码直接依赖src下的代码
go get
下载最新版本的包到src目录下
存在的弊端:无法实现package的多版本控制,如果需要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
下面是详细一些的解释:
go.mod
version的两种类型:
- 语义化版本
- 基于commit伪版本
indirect
对于没有直接表示的模块会在go.mod
中加上// indirect
,例如(A->B->C)
incompatible
- 主版本2+模块会在模块路径增加
/vN
后缀 - 对于没有
go.mod
文件并且主版本2+的依赖,会加上+incompatible
,表示可能会存在一些不兼容的代码逻辑
一个例子:
依赖分发-回源
说人话就是这个依赖要去哪里下载,如何下载的问题。
实际上是用Proxy
来缓存,保证了依赖的稳定性;
依赖分发-变量 GOPROXY
查找依赖的逻辑:如果最后都没找到就会回到第三方Direct
工具 go get
三. 单元测试
单元测试简单例子
test内容,预期结果:输出Tom,实际得到的结果Jerry
go如何进行单元测试:在Terminal中,cd到目标测试文件所在的目录下,test文件要以_test结尾,在Terminal中输入go test,默认会将该目录下的测试文件全都测试一遍
package go_test
import "testing"
func HelloTom() string {
return "Jerry"
}
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
if output != expectOutput {
t.Errorf("Expected %v do not match actual %v", expectOutput, output)
}
}
测试结果
如果将HelloTom中的return "Jerry"改成"Tom"的话,测试结果就是下图,PASS
用go get拉取模块包
go get 命令可以借助代码管理工具通过远程拉取或更新代码包及其依赖包,并自动完成编译和安装。 整个过程就像安装一个 App 一样简单。
设置好代理,不然会请求不到
go mod init gorm_learn/ go mod init创建了一个新的go.mod module gorm_learn
也不知道这一步需不需要操作
提供管理员身份在终端该go文件目录下执行go get github.com/stretchr/testify/assert,不然会报权限的错误
导入
"github.com/stretchr/testify/assert"
之后就可以使用assert包来单元测试了
但是此时又遇到了权限问题,还是得以管理员身份运行终端,cd到测试文件的目录下进行go test
package go_test
import (
"github.com/stretchr/testify/assert"
"testing"
)
func HelloTom() string {
return "Jerry"
}
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
assert.Equal(t, expectOutput, output)
}
终端的测试结果如下图
这样就完成测试了
单元测试---文件处理
直接上代码,注意要在同源目录下创建一个file log
存在本地依赖文件
package main
import (
"bufio"
"github.com/stretchr/testify/assert"
"os"
"strings"
"testing"
)
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")
return destLine
}
func TestProcessFirstLine(t *testing.T) {
firstLine := ProcessFirstLine()
assert.Equal(t, "line00", firstLine)
}
如果log文件中写的是line11
测试结果如下
如果log文件中写的是line22
测试结果如下
单元测试---Mock
对文件打桩,不存在本地依赖文件