Go语言学习-----工程进阶(Day3)

一.并发编程

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

对文件打桩,不存在本地依赖文件

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值