这是我参与「第五届青训营」伴学笔记创作活动的第 2 天
Go依赖管理与工程实践 | 青训营笔记
1、并发编程
并发VS并行
由于现在硬件多为多核
Go 可以充分发挥多核优势,高效运行
协程Goroutine
Go 的 Goroutine 是用户态的,其协程栈占用仅有 KB 级别,十分节约系统资源;但不同的是,Goroutine 将协程和并发简化到了仅需一个 go 关键字即可完成,而不像其他语言的协程一样极其繁琐复杂。
- 协程:用户态,轻量级线程,栈KB级别。
- 线程:内核态,线程跑多个协程,栈MB级别。
package main
import (
"fmt"
"time"
)
func HelloPrint(i int) {
// println("Hello goroutine : " + fmt.Sprint(i))
fmt.Println("Hello goroutine :", i)
}
func HelloGoroutine() {
for i := 0; i < 5; i++ {
go func(j int) {
HelloPrint(j)
}(i)
}
// time.Sleep()的作用是:保证了子协程在执行完之前,主协程不退出。
time.Sleep(time.Second)
}
func main() {
HelloGoroutine()
}
- 即使你在你的程序中不使用任何一个
go关键字,也依然存在一个协程在运行:这个协程就是main函数自己。 - 当我们有多个子协程执行时,应该等待这些协程全部执行完毕后,再结束主协程
- 只有在一段程序有空闲时间的时候,另一端程序才有机会抢过执行权,执行自己。标准库中
WaitGroup提供解决方案
通信共享内存
通道Channel
- 无缓存 Channel 意味着,一个数据的发送必须等待另一端代码的接收,如果没有人接收发送的数据,那么发送端便会被永远阻塞。
- 有缓存Channel 则在缓冲区满时,数据未被接收而阻塞
向Channel中发送数据:
ch <- v //v是一个变量
从Channel中接收数据:
v := <-ch//赋值给变量v
也可以使用 for range 来取出Channel中所有数据
for i := range ch {
fmt.Println(i)
}
for range将会始终读取一个 Channel 中发送的数据,直到该 Channel 被关闭。Channel被使用完时应该调用close关闭
close(ch)
并发安全Lock
Mutex互斥锁实现数据同步
var (
x int64
lock sync.Mutex
)
func addWithLock() {
for i := 0; i < 2000; i++ {
lock.Lock()
x += 1
lock.Unlock()
}
}
func main() {
x = 0
for i := 0; i < 5; i++ {
go addWithLock()
}
time.Sleep(time.Second)
println("WithLock:", x)
}
并发锁的原理是,当第一次调用 Lock 方法时,什么都不会发生,但当第二次调用 Lock 方法时,该调用便会立刻阻塞协程,直到有程序调用 Unlock 方法解锁。
WaitGroup
package main
import (
"fmt"
"sync"
)
func HelloPrint(i int) {
fmt.Println("Hello WaitGroup :", i)
}
func ManyGoWait() {
var wg sync.WaitGroup //声明了一个名字叫wg的WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func(j int) {
defer wg.Done()
HelloPrint(j)
}(i)
}
wg.Wait()
}
func main() {
ManyGoWait()
}
WaitGroup它内部维护一个计数器:
-
通过调用
Add方法,向WaitGroup的计数器添加指定值; -
通过调用
Wait方法阻塞当前协程,这会使得协程陷入无限的等待; -
通过调用
Done方法使WaitGroup内部的计数器 -1,直到计数器值为 0 时,先前被阻塞的协程便会被释放,继续执行接下来的代码或是直接结束运行。
2、依赖管理
对引入第三方依赖库进行统一管理
GO依赖管理的演进过程
对于 Go 的依赖管理来说,经历了 GOPATH,Go Vender,Go Module 三部分的演进。
GOPATH
最初,Go 直接将依赖库源码扔进 GOPATH 的 src 文件夹以作为项目依赖。
GOPATH 是一个环境变量,指向一个目录,作为项目的编译产出目录和依赖目录。这是一个公共环境变量,也就意味着,所有项目都依赖于同一个 GOPATH,这就会导致这样的问题:如果项目 A 依赖于依赖库 Lib 的版本 1,而项目 B 依赖于同一个依赖库的版本 2,由于 GOPATH 并没有任何版本管理措施,就会导致编译出错。
Go Vender
于是,Go 引入了 Go Vendor,通过在项目目录下新建 vendor 文件夹,并存放依赖库文件副本的方式,使得不同项目可以依赖不同的依赖库版本,解决了版本冲突的问题。
值得一提的是,如果无法在 vendor 文件夹中找到项目所需的依赖文件,那么 Go 会尝试回到 GOPATH 查找。
Go Vendor 的引入看似解决了版本问题,但是实际上依然造成了问题:如果项目 A 引入了 项目 B 和项目 C 作为依赖库,而后两者又共同依赖了项目 D 的不同版本,那么由于 B,C,D 作为项目 A 的依赖依然被同时存在同一个 vendor 文件夹中,依旧导致了依赖冲突。
Go Module
终于,Go 采用了和 JavaScript(NodeJS)的 npm 类似的包管理方案 —— 这就是 Go Module。
Go Module 通过项目路径中的 go.mod 文件(类似于 npm 的 pakcage.json 声明所需依赖的名称和版本范围),然后,通过 go.sum 文件记录项目实际使用的依赖和版本(类似于 npm 的 package-lock.json)。
我们没有必要像 Java 的 Maven/Gradle 那样手动编辑配置文件指定依赖,Go 为我们提供了 go get 和 go mod 两条指令来方便的添加和移除项目中的依赖。
go.mode 文件详解
一个合法的go.mod
module example/project/app
go 1.16
require (
example/lib1 v1.0.2
example/lib2 v1.0.0 // indirect
example/lib3 v0.1.0-20190725025543-5a5fe074e612
example/lib4 0.0.0-20180306012644-bacd9c7efldd // indirect
example/lib5/v3 v3.0.2
example/lib6 v3.2.0+incompatible
)
module example/project/app标识了依赖管理的基本单元;go 1.16指定了 Go 原生库(标准库)的版本,此处我们指定版本为1.16require内则指定了单元依赖,格式是[Module Path] [Version/Pseudo-version]
对于一个依赖项,我们首先指定其名称(路径),然后指定所需的版本。版本号应当按照语义化版本(MAJOR.MINOR.PATCH)的格式填入,例如 v1.0.2;
或者,我们可以填入一个基于 commit 的伪版本,代表我们需要来自某个 commit 的依赖库版本,它的格式是 vx.0.0-yyyymmddhhmmss-abcdefgh1234,其中 yyyymmddhhmmss 是提交 Commit 的时间,而 abcdefgh1234 则是该 commit 的哈希值。
有的依赖可能会使用 // indirect 注释标识,这意味着该依赖并非由项目直接引入,而是透过其他依赖间接引入(例如 A 项目引入了 B 依赖,B 依赖依赖于依赖 C,那么依赖 C 就是 项目 A 的间接依赖)
有的依赖可能会在版本末尾添加 +incompatible 标识,这是为了兼容非语义化版本所致。
依赖分发
go get指令
go mod指令
3、单元测试
首先,GO是内置单元测试支持的。
所有以_test.go结尾的代码文件会被GO识别为单元测试文件
一个单元测试函数的函数名应当以Test开头,并包含*testing.T形参
通过 func TestMain(m *testing.M) 函数对测试数据进行初始化,并调用 m.Run() 运行单元测试。
以下代码是一个简单的单元测试例子,测试 HelloTom 函数是否正常返回值 Tom:
// In xxx.go:
func HelloTom() string {
return "Jerry"
}
// In xxx_test.go:
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
if output != expectOutput {
t.Errorf("Expected %s do not match actual %s", expectOutput, output)
}
}
运行测试
go test [flags] [packages]
还有引入依赖库加快单元测试开发
通过 testify/assert 库进行覆盖率测试
通过 bouk/monkey 库对数据进行 Mock
基准测试
基准测试是指测试一段程序的运行性能及耗费CPU的程度。而我们在实际项目开发中,经常会遇到代码性能瓶颈,为了定位问题经常要对代码做性能分析,这就用到了基准测试。使用方法类似于单元测试
下面代码模拟了一个负载均衡的例子,在 10 个服务器中随机返回数据,我们将对 Select 方法分别进行串行和并行的基准测试:
// In xxx.go:
import "math/rand"
var ServerIndex [10]int
func InitServerIndex() {
for i := 0; i < 10; i++ {
ServerIndex[i] = i + 100
}
}
func Select() int {
return ServerIndex[rand.Intn(10)]
}
// In xxx_test.go:
package main
import "testing"
func BenchmarkSelect(b *testing.B) {
InitServerIndex()
b.ResetTimer() // 重置计数器是因为 InitServerIndex 不应包含在测试时间内
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()
}
})
}
使用 go test -bench= 指令运行基准测试,得到结果
4、项目实战
通过使用高性能 go web 框架 Gin 配合 MVC(Model-View-Controller)模式,开发了一个有一个路由的简易论坛后端。(以后补充)
未完待续。。。