二、GO语言工程实践课后作业| 青训营

65 阅读3分钟

2.1 语言进阶

2.1.1 Goroutine(协程)

day1里面已经有学过这个概念,它体现的是一个 “快” 字,也即使用并发的形式。

快速打印hello goroutine:0~4:

package main

import (
	"fmt"
	"time"
)

func hello(i int) {
	println("hello goroutine:" + fmt.Sprint(i))
}

func main() {
	for i := 0; i < 5; i++ {
		go func(j int) {
			hello(j)
		}(i)
	}
	time.Sleep(time.Second)
}

运行结果(随机):

hello goroutine:4
hello goroutine:1
hello goroutine:3
hello goroutine:2
hello goroutine:0

值得记住

1、fmt.Sprint()函数

fmt.Sprint()函数是Go语言中的一个格式化输出函数,它用于将给定的参数格式化成字符串并返回结果。这个函数可以接收任意数量的参数,并将它们按照默认的格式转换成一个字符串

2、go func(j int) {
		hello(j)
	}(i)

GoRoutine结构中,i表示输入的参数,其中j是通过参数i来确定。

3time.Sleep(time.Second)

暂停执行1秒钟。

main()函数中,我们使用了go func(j int) { ... }(i)来创建了5个goroutine,并且这些goroutine会并发地执行hello(j)函数来输出信息。然而,问题在于主goroutine(即main()函数本身)启动了这些goroutine后,没有等待它们执行完成就立即终止了。这意味着在主goroutine终止后,程序可能会立即退出,而这些创建的goroutine还没有来得及执行完毕

为了确保所有的goroutine有足够的时间来执行输出操作,我们在主goroutine中添加了time.Sleep(time.Second)操作。这样做是为了让主goroutine暂停执行1秒钟(即等待1秒钟),确保创建的goroutine有足够的时间去执行hello(j)函数输出信息。

在实际生产环境中,这种使用time.Sleep()来等待goroutine执行完毕的方式并不是最好的做法,因为休眠时间的长短不好确定。更好的方法是使用Go语言提供的同步机制(如2.1.4sync.WaitGroup)来等待所有的goroutine执行完毕。后面再来讲。

2.1.2 Channel

make(chan int):创建一个整数类型的无缓冲通道。无缓冲通道意味着在发送数据时,发送方会阻塞直到有接收方接收数据。
make(chan int,2):创建一个整数类型的有缓冲通道,其缓冲区大小为2。有缓冲通道可以在缓冲区未满时进行发送,只有在缓冲区已满时才会阻塞发送方。

这段代码涉及了通道channel的使用和并发处理,主要功能是:一个子协程负责发送0~9数字,另一个子协程负责计算输入数字的平方,主协程负责输出最后的平方数:

package main

func main() {
	src := make(chan int)
	dest := make(chan int, 3) 
	go func() {
		defer close(src)
		for i := 0; i < 10; i++ {
			src <- i
		}
	}()
	go func() {
		defer close(dest)
		for i := range src {
			dest <- i * i
		}
	}()
	for i := range dest {
                //可能执行的复杂操作
                //所以给dest加缓冲区的原因就在于执行复杂操作可能会导致这个地方执行速度没那么快,所以要多点空间来存放执行速度快的协程输出的结果。
		println(i)
	}
}

执行结果:

0
1
4
9
16
25
36
49
64
81

2.1.3 Lock并发安全

协程是通过共享内存来执行,所以在执行的过程中可能会遇到一些情况,实际上下面在操作的时候很像我们操作系统中的PV操作,每次使用临界区都要加锁,使用完后释放锁

对变量执行2000次+操作,5个协程并发执行,我们写两个函数,一个上锁一个不上锁:

实际输出结果:

WithoutLock: 8524
WithLock: 10000

实际上就会发现,由于共享空间,所以没有上锁的那个函数输出的值是不固定的。

值得记住

1var (
	x    int64
	lock sync.Mutex
)

var()用来定义全局变量sync.Mutex是一种互斥锁类型,用于在多个goroutine之间实现数据的互斥访问。可以参考PV操作中的P(mutex),V(mutex);

所以,在使用到并发开发的时候,就应该考虑到共享内存的问题

2.1.4 WaitGroup

回到上文time.Sleep()中存在的问题,使用WaitGroup可以很好的解决。

同样的题目:快速打印hello goroutine:0~4:

package main

import (
	"fmt"
	"sync"
)

func hello(i int) {
	println("hello goroutine:" + fmt.Sprint(i))
}

func main() {
	var wg sync.WaitGroup
	wg.Add(5)
	for i := 0; i < 5; i++ {
		go func(j int) {
			defer wg.Done()
			hello(j)
		}(i)
	}
	wg.Wait()
}

实际结果:

hello goroutine:0
hello goroutine:3
hello goroutine:4
hello goroutine:2
hello goroutine:1

值得记住

1var wg sync.WaitGroup

sync.WaitGroup是Go语言标准库中提供的一种并发控制机制,用于等待一组goroutine的完成。它可以确保在所有指定的goroutine执行完成之前,主goroutine(或其他等待的goroutine)会一直阻塞

WaitGroup是一个结构体类型,位于sync包中。它包含了用于等待的计数器。主要有三个方法:Add()Done()Wait()。Wait()方法一定不能省略,否则主协程还是会直接执行结束,不会阻塞,造成无结果输出。

2、go func(j int) {
		defer wg.Done()
		hello(j)
	}(i)

defer是为了确保在当前每一个GoRoutine执行完毕后,再执行wg.Done()函数,这样就能理解defer函数的真正意义。

2.2 依赖管理

2.2.1 go项目依赖

在Go语言中,一个Go项目的依赖是指项目代码所依赖的其他外部包或模块。这些外部包或模块是其他开发者编写的,它们提供了一些常用的功能、库或工具,可以被项目代码引用和使用

Go语言引入了Go Modules(Go模块)的概念,它是Go语言的官方依赖管理解决方案。使用Go Modules,你可以在项目中指定你的依赖关系,以及具体的依赖版本。

一个典型的Go项目依赖可以分为两类:

  1. 标准库依赖:Go语言本身提供了丰富的标准库,这些库包含了许多常用的功能,如字符串处理、网络通信、文件操作等。标准库是每个Go项目都默认依赖的,无需额外配置。
  2. 第三方依赖:除了标准库,Go项目通常还会依赖一些第三方包或模块,这些包可以在开源代码仓库(例如GitHub)中找到。你可以通过Go Modules的配置,指定你的项目需要哪些第三方包以及它们的版本。

项目的根目录中,会有一个名为go.mod的文件,这个文件用于记录项目的依赖信息。它会列出所有项目所使用的外部包的导入路径以及具体的依赖版本。例如:

module example.com/myproject

go 1.17

require (
    github.com/go-http-utils/headers v1.0.0
    github.com/spf13/viper v1.8.3
    golang.org/x/text v0.3.6
)

2.2.2 中心仓库管理依赖库

Go的中心仓库是指存放Go模块的依赖库信息的远程服务器。Go模块是Go语言官方引入的依赖管理系统,它允许开发者通过模块路径来获取和管理项目的依赖。

在Go Modules中,当我们使用import语句导入一个外部包时,Go编译器会在本地的go.mod文件中查找依赖的版本信息。如果本地没有这个依赖的版本信息,那么Go会通过网络请求去查询中心仓库,以获取对应的依赖版本信息

Go的中心仓库是一个全球范围内的分布式系统,由一组公共中心仓库和私有中心仓库组成。公共中心仓库由Go语言社区维护,包含了许多开源的Go模块,任何人都可以免费使用。其中,最著名的公共中心仓库是"proxy.golang.org"。

"proxy.golang.org"是Go的中心代理仓库,它位于谷歌云平台上。它的作用是提供一个全球性的代理服务,用于缓存和分发公共中心仓库中的Go模块。这样,当有多个开发者或者项目需要同一个Go模块时,"proxy.golang.org"可以将这个模块缓存起来,避免重复下载,提高依赖下载的速度和效率

使用"proxy.golang.org"代理仓库的好处包括:

  1. 加速依赖下载:因为它会缓存公共中心仓库中的Go模块,所以当多个项目依赖相同的模块时,可以避免重复下载,加快依赖下载速度。
  2. 提高稳定性:由于模块被缓存,即使公共中心仓库不可用或者模块的版本发生变化,"proxy.golang.org"仍然能够提供稳定的依赖下载服务。
  3. 减轻公共中心仓库的负载:因为"proxy.golang.org"缓存了很多Go模块,可以减轻公共中心仓库的负载压力,使得公共中心仓库更稳定和可靠。

当Go编译器需要获取依赖版本信息时,它会先尝试从本地的go.mod文件查找,如果没有找到则会向"proxy.golang.org"发起请求。开发者无需手动使用或配置"proxy.golang.org",Go编译器会自动处理这一切

所以,Go的中心仓库是存放Go模块依赖库信息的远程服务器,其中最著名的公共中心仓库是"proxy.golang.org"。它提供全球性的代理服务,用于缓存和分发公共中心仓库中的Go模块,加速依赖下载,提高稳定性,减轻公共中心仓库的负载压力。开发者在使用Go Modules时,不需要手动使用或配置"proxy.golang.org",Go编译器会自动处理依赖下载。

2.2.3 go mod

go mod init
go mod download  
go mod tidy

1、go mod download是Go语言命令行工具中的一个命令,用于下载指定模块依赖的所有包到本地缓存,而不会将它们安装到GOPATH目录中。在Go Modules(Go模块)中,当你使用go get或者go install等命令来获取或安装包时,Go语言会自动下载并安装这些包及其依赖到GOPATH中。而go mod download命令则不会将这些包安装到GOPATH,它只会下载这些包到本地缓存目录(默认位于$GOPATH/pkg/mod$HOME/go/pkg/mod)。

2、go mod tidy是Go语言中用于整理和清理项目依赖的命令。它是Go Modules(Go模块)的一部分,用于维护和管理项目的依赖关系。

你在一个Go项目中使用Go Modules时(通过在项目中运行go mod init命令初始化Go模块),Go会自动跟踪你的依赖,并将依赖的版本信息保存在go.mod文件中。然而,随着项目的开发,可能会添加、删除或修改依赖,或者手动修改go.mod文件。为了确保项目的依赖信息与实际使用的依赖一致,可以使用go mod tidy命令来自动清理并更新go.mod文件。它可以实现删除未使用的依赖,更新依赖版本,添加缺失依赖(如果项目代码中使用了新的依赖,但还未添加到go.mod文件中,go mod tidy会自动将这些依赖添加到go.mod文件)

2.3 测试

2.3.1 单元测试

1、规则

package main

import "testing"

func HelloTom() string {
	return "Jerry"
}

func TestHelloTom(t *testing.T) {
	output := HelloTom()
	expectOutput := "Tom"
	if output != expectOutput {
		t.Errorf("Expected %s do not match actual %s", expectOutput, output)
	}
}

输出结果:

=== RUN   TestHelloTom
    tom_test.go:13: Expected Tom do not match actual Jerry
--- FAIL: TestHelloTom (0.00s)

FAIL


Process finished with the exit code 1

源代码再输出错误时候使用的是t.Error()函数,执行报错了。实际上应该使用t.Errorf()函数来格式化字符串,输出错误信息。

2、使用assert断言库

使用这个断言库的时候需要现在项目里面导入这个库

go get github.com/stretchr/testify/assert

其中,assert.Equal()语法规则:

assert.Equal(t, expected, actual)
  • t 是测试函数的*testing.T参数,它用于报告测试失败和日志输出。
  • expected 是期望的值。
  • actual 是实际的值。

实际输出:

=== RUN   TestHelloTom
    tom_test.go:15: 
        	Error Trace:	C:/Users/余泽宇/Desktop/go-study/go-day/to   m_test.go:15
        	Error:      	Not equal: 
        	            	expected: "Tom"
        	            	actual  : "Jerry"
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -1 +1 @@
        	            	-Tom
        	            	+Jerry
        	Test:       	TestHelloTom
--- FAIL: TestHelloTom (0.00s)


Expected :Tom
Actual   :Jerry
<Click to see difference>


FAIL


Process finished with the exit code 1

3、覆盖率

实际上覆盖率简单来说就是:被测试函数共有多少行代码被调用÷被测试函数的中代码行数

语法规则:

go test -cover

??

2.3.2 Mock测试

2.3.3 基准测试

基准测试(Benchmark Test)是一种用于测量代码性能的测试方法。在Go语言中,基准测试是通过Go的测试框架自带的工具实现的。基准测试函数必须以Benchmark为前缀,并且参数列表中必须包含一个指向testing.B的指针参数

func BenchmarkAdd(b *testing.B) {
	
}

服务器负载均衡的例子:

package main

import (
	"math/rand"
	"testing"
)

var ServerIndex [10]int

func InitServerIndex() {
	for i := 0; i < 10; i++ {
		ServerIndex[i] = i + 100
	}
}

func Select() int {
	return ServerIndex[rand.Intn(10)]
}

// 对Select做一个基准测试
func BenchmarkSelect(b *testing.B) {
	InitServerIndex()  
	b.ResetTimer()  
	for i := 0; i < b.N; i++ {
		Select()
	}
}

func BenchmarkSelectParallel(b *testing.B) {
	InitServerIndex()
	b.ResetTimer()
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			Select()
		}
	})
}

值得记住

1、BenchmarkSelect(b *testing.B) 函数是用来测试Select()函数的性能,使用b.ResetTimer()重置计时器是因为前面InitServerIndex()函数的启动不能算在Select()函数损耗的时间内。

2b.N

b.N是基准测试框架提供的一个参数,用于指定测试的运行次数。基准测试结果将显示每次运行的耗时,并计算平均耗时。

3b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			Select()
		}
	})

这是另一个基准测试函数用来实现并行测试的函数。b.RunParallel()接受一个函数作为参数,并在多个goroutine中同时运行该函数。这样可以利用多核处理器进行并行测试,加快测试速度。

for pb.Next():这是一个循环,表示在每次基准测试迭代中运行测试代码。pb.Next()会返回true,直到基准测试运行次数完成,此时会返回false,循环结束。