Go语言进阶 | 青训营笔记

136 阅读4分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天

Go语言进阶 - 工程进阶

Go语言进阶和依赖管理

1.1 Goroutine协程

go语言里面的协程不等于线程

  • 协程属于用户态,线程属于内核态
  • 协程是轻量级线程,线程可以跑多个协程
  • 协程的内存占用是KB级别,线程是MB级别
  • 协程的创建和销毁的开销更小

在go语言里建立一个协程只需要在func前加一个go即可

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

1.2 CSP

CSP 并发模型是上个世纪七十年代提出的,用于描述两个独立的并发实体通过共享 channel(管道)进行通信的并发模型。

Go语言就是借用 CSP 并发模型的一些概念为之实现并发的,但是Go语言并没有完全实现了 CSP 并发模型的所有理论,仅仅是实现了 process 和 channel 这两个概念。

process 就是Go语言中的 goroutine,每个 goroutine 之间是通过 channel 通讯来实现数据共享。

go希望协程是通过通信来共享内存,而不是通过共享内存来通信

1.3 channel

如何建立一个channel呢

//建立channel方式
make(chan int)//无缓冲通道
make(chan int,2)//有缓冲通道
//A子协程发送数字
//B子协程计算平方和
//主协程输出平方和
func CalSquare() {
	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 {
		//复杂操作
		println(i)
	}
}

无缓冲通道----放一个拿一个(同步通信)

有缓冲通道----放满然后等拿(生产消费),一般用于生产比消费速度慢的情况下,可以缓解一下消费速度和生产速度矛盾的问题

1.4 Lock

因为会出现多个协程对同一个资源操作的情况,和其他语言,go的基本库提供了锁

官方原文说低级同步才用互斥锁这种

用法比较简单,和其他语言差不多

var lock sync.Mutex
lock.Lock()
lock.Unlock()

1.5 waitGroup

跟Java的CountDownLatch差不多

waitGroup有几个方法
Add(delta int) 表示计数器+delta
Done() 计数器-1
Wait() 阻塞直到计数器为0

依赖管理

2.1 Go 依赖管理推进

2.1.1 GOPATH

GOPATH工作目录可以理解为每个人有一套按照标准格式的目录。

GOPATH目录下有三个包

  • bin 存放项目编译的二进制文件

  • pkg 存放项目编译的中间产物

  • src 存放项目源码

项目代码直接依赖src的代码

go get直接下载最新版本的包到src目录下

但是有一个弊端就是,A项目和B项目依赖了同一个包,但是版本不同,B依赖的包可能没有对应的功能,这会导致A和B不能同时构建成功。

2.1.2 Go Vendor

Go Vendor其实就是将依赖的包,特指外部包,复制到当前工程下的vendor目录下,这样go build的时候,go会优先从vendor目录寻找依赖包。

但是会出现子包不兼容的情况,比如主项目依赖了A和B项目,A和B项目依赖了同一个包,但是版本兼容。而且无法控制版本

2.1.3 Go Module

通过go.mod文件管理依赖包版本

通过go get/go mod指令管理依赖包

2.2依赖管理三要素

  • 配置文件,描述依赖 go.mod
  • 中心仓库管理依赖库 Proxy
  • 本地工具 go get/mod

2.3.1 依赖配置-go.mod

module github.com/Moonlight-Zhao/go-project-example //依赖管理单元

go 1.16 //原生库

require (  //单元依赖
   github.com/gin-contrib/sse v0.1.0 // indirect
   github.com/gin-gonic/gin v1.8.2 // indirect
   github.com/golang/protobuf v1.5.2 // indirect
   github.com/jinzhu/now v1.1.5 // indirect
   github.com/json-iterator/go v1.1.12 // indirect
   github.com/kr/pretty v0.3.0 // indirect
   github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
   github.com/rogpeppe/go-internal v1.8.0 // indirect
   github.com/ugorji/go v1.2.7 // indirect
   go.uber.org/atomic v1.9.0 // indirect
   go.uber.org/multierr v1.8.0 // indirect
   go.uber.org/zap v1.21.0 // indirect
   gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
   gopkg.in/gin-gonic/gin.v1 v1.3.0
   gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
   gopkg.in/go-playground/validator.v8 v8.18.2 // indirect
   gopkg.in/yaml.v2 v2.4.0 // indirect
   gorm.io/driver/mysql v1.3.3 // indirect
   gorm.io/gorm v1.23.4 // indirect
)

每一个单元依赖由module路径 + 版本号来唯一标识

2.3.2 依赖配置-version

版本号主要有两个版本规则

语义化版本

${MAJOR}.${MINOR}.${PATCH}
名称含义
MAJOR不同的MAJOR版本表示是不兼容的API。因此即使是同一个库,MAJOR版本不同也会被认为是不同的模块
MINOR通常是新增函数或功能,向后兼容
PATCH一般是修复bug

Commit版本 每次提交 commit 后,Go 都会默认生成一个伪版本号:

v0.0.0-yyyymmddhhmmss-abcdefgh1234

如:v1.0.0-20220517152630-c38fb59326b7

名称含义
v0.0.0版本前缀和语义化版本是一样的
yyyymmddhhmmss时间戳,提交Commit的时间
abcdefgh1234校验码,包含12位的哈希前缀

2.3.3 依赖配置-indirect

这是一个特殊标识符,表示 go.mod 对应的当前 module 没有直接导入的包,也就是非直接依赖 (即间接依赖) 。

image.png 例如,一个依赖关系链为:A->B->C 。其中,A->B 是直接依赖;而 A->C 是间接依赖。

2.3.4 依赖配置-incompatible

incompatible也是一个特殊标识符。对于 MAJOR 主版本在 V2 及以上的模块,go.mod 会在模块路径增加 /vN 后缀 (如下图中 example/lib5/v3 v3.0.2 )。这能让 Go Module 按照不同的模块来处理同一个项目不同 MAJOR 主版本的依赖。

由于 Go Module 是在 Go 1.11 才实验性地引入,所以在这个更新提出之前,已经有一些仓库打上了 V2 或者更高版本的 tag 了。 为了兼容这部分仓库,对于没有 go.mod 文件并且 MAJOR 主版本在 V2 及以上的依赖,会在版本号后加上 +incompatible 后缀。表示可能会存在不兼容的源代码。

image.png

2.3.5 依赖配置-依赖图

image.png Go 选择最低的兼容版本

2.3.6 依赖分发-回源

依赖分发,即依赖从何处下载、如何下载的问题。 Go 的依赖绝大部分托管在 GitHub 上。Go Module 系统中定义的依赖,最终都可以对应到 GitHub 中某一项目的特定提交 (commit) 或版本。 对于 go.mod 中定义的依赖,则直接可以从对应仓库中下载指定依赖,从而完成依赖分发。

image.png

弊端

直接使用 GitHub 仓库下载依赖存在一些问题:

  • 首先,无法保证构建稳定性。代码作者可以直接在 GitHub 上增加/修改/删除软件版本。

  • 无法保证依赖可用性。代码作者可以直接在 GitHub 上删除代码仓库,导致依赖不可用。

  • 第三,如果所有人都直接从 GitHub 上获取依赖,会导致 GitHub 平台负载压力。 解决方案-Proxy

  • Go Proxy 就是解决上述问题的方案。Go Proxy 是一个服务站点,它会缓存 GitHub 中的代码内容,缓存的代码版本不会改变,并且在 GitHub 作者删除了代码之后也依然可用,从而实现了 “immutability” (不变性) 和 “available” (可用的) 的依赖分发。

  • 使用 Go Proxy 后,构建时会直接从 Go Proxy 站点拉取依赖。如下图所示。

image.png

2.3.7工具

go get工具

go get example.org/pkg +...

go mod工具

指令功能
init初始化,创建go.mod文件
download下载模块到本地缓存
tidy增加需要的依赖,删除不需要的依赖

3 测试

3.1 单元测试

单元测试主要就是对模块内的函数进行功能测试。

3.1.1 规则

  • 所有测试文件以_test.go结尾

image.png

  • 函数符合func Testxxx(*testing.T)

image.png

  • 初始化逻辑放到 TestMain 中

image.png

之后只需要将工程文件和测试文件放在一起,然后终端使用go test即可开始测试

此外,我们可以利用assert断言来简单地测试。

代码覆盖率 指的就是源代码被测试的比例和程度,一般来说,代码覆盖率越高越好

3.2 Mock测试

mock主要用来操作预期数值不依赖于本地文件,就是我们可以手动给这个函数让它返回固定的值,从而实现测试效果。

一般操作是mock打桩

image.png 因为我没有深究过,我感觉用monkey.patch()就是临时重写一个方法,让它返回我们想要的值。

3.3 基准测试

基准测试和单元测试差不多

  • 基准测试的函数必须以 Benchmark 开头,必须是可以导出的
  • 入参是*testing.B
  • 基准测试函数不能有返回值

image.png image.png InitServerIndex()是为了初始化服务器索引,初始化后我们需要重置时间,因为这一部分不是测试所花的时间。

image.png

之后一个Gin框架的案例我就不记录了,后续课程应该有详细讲解。

总结一下

今天的课主要关于go语言的一些协程知识,依赖管理以及一些测试方法。学到了之前没有学过的一些测试方法,受益匪浅。