这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天
今天青训营的内容主要分为四块:语言进阶、依赖管理、测试和项目实战。语言进阶主要讲了如何使用语言进行更高效、简洁的编程,依赖管理则讲了如何管理项目依赖关系,测试则讲了如何使用测试来保证项目的质量,项目实战则是对所学知识的实际应用。
下面是今天的学习笔记
一、语言进阶
主要讲解了Go语言下的并发编程和并发安全。
1. 并发
Go语言提供了一种简单而强大的并发编程模型,它使用了 goroutines 和 channels 来实现。
- goroutines是轻量级的线程(协程),在用户态创建。可以通过 go 关键字来启动一个新的 goroutine。
- channels是一种类型安全的通信机制,可以在 goroutines 之间传递消息。在go中推荐使用通道来共享内存,而不是通过共享内存来达到通信的目的。
以下代码演示了如何使用 goroutines 和 channels 实现一个简单的并发程序
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 {
fmt.Println(i)
}
}
在这段代码中,声明了两个channel:src和dest,并通过go关键字启动了两个子协程。在两个子协程中,一个协程不断的向管道src中输入数据 “ i ”,另一个协程不断的从管道src中取出数据并输入到管道dest中,最后主协程再从管道dest中取出数据。通过这样一段代码,很清晰地能看到两个协程并发的对数据进行通信、交换,最后再返回给主协程,提高程序的执行效率。
2. 并发安全
在这一节中主要讲解了若干协程并发时,可能产生的数据安全问题和Go中使用sync包下的Mutex和WaitGroup解决问题的方法。
首先来看一个例子
var x int64 = 0
func addWithoutLock() {
for i := 0; i < 2000; i++ {
x += 1
}
}
// 执行完函数后,x的预期值为10_000
func add() {
for i := 0; i < 5; i++ {
go addWithoutLock()
}
}
我在我的环境中上执行了10次该段程序,很明显有3次的执行结果没有达到预期,这就是潜在的并发安全问题。在这段程序中,我们没有对临界区或者说临界值x做任何保护措施,因此开启的5个子协程都可以在任意时间下访问x并对x进行读写,这就导致了x可能会出现如图所示的结果。
接下来将会用到sync包中的Mutex
sync.WaitGroup是Go语言中用于等待一组goroutine结束的工具。它包含一个计数器,用于跟踪还有多少个 goroutine需要等待结束。使用者可以通过Add方法来增加计数器的值,表示有新的goroutine需要等待;通过Done 方法来减少计数器的值,表示有一个goroutine已经结束;最后通过Wait方法等待所有被等待的goroutine结束。
var locker sync.Mutex
var x int64 = 0
func addWithLock() {
lokcer.Lock()
x += 1
locker.Unlock()
}
func add() {
for i := 0; i < 5; i++ {
go addWithLock()
}
}
在这段程序中,用到sync下的Mutex,在对临界区之前locker会对临界区加锁,只有第一个执行addWithLock函数的协程能够获得操作临界区的权限,此时其他协程都会被锁给阻塞而持续的等待获取临界区的权限。
func hello(n int) {
fmt.Println("hello goroutine:", n)
}
func main() {
for i := 0; i < 5; i++ {
hello(i)
}
}
这段程序会串行的依次打印5条 “ hello goroutine: 1··· ”而如果想要加快程序的执行速度,那么就让这五次函数并发的执行就可以了。
下面会用到sync包下的WaitGroup
var wg sync.WaitGroup
func hello(n int) {
fmt.Println("hello goroutine:", n)
}
func main() {
wg.Add(5)
for i := 0; i < 5; i++ {
go func(j int){
defer wg.Done()
hello(j)
}(i)
}
wg.Wait()
}
这样我们就一次性的开启了5个协程去打印这段话,并且用wg.Wait()函数让主协程等待这5个协程的结束。
二、依赖管理
Go语言的依赖管理是指管理Go语言项目所依赖的其他包(第三方包)
1. Go语言依赖管理的发展历程
- 初期,Go语言采用的是手动管理依赖的方式,开发者需要自己手动下载依赖包并放到指定的目录中。
- 2011年,Go语言发布了go get命令,可以通过它来自动下载依赖包并管理依赖。
- 2013年,Go语言发布了go dep工具,它是第一个用于Go语言的依赖管理工具,使用了预设的vendor目录来管理依赖。
- 2016年,Go语言官方发布了vendor experiment,提供了一种新的方式来管理依赖,即将依赖包放入项目的vendor目录中。
- 2017年,Go语言正式在1.8版本中提供了内置的vendor机制,使得开发者可以直接使用go build和go test命令来编译和测试项目,而不需要使用其他工具来管理依赖。
目前Go语言推荐的依赖管理工具是go mod,该工具是 Go 1.11 引入的一个新的标准工具,可以解决 Go 语言项目依赖管理的问题。
2. Go Module
Go Module主要包括三个要素:
- go.mod 配置文件,用于描述依赖关系
- proxy 中心仓库,用于管理依赖库
- go get/mod 本地依赖管理工具
1) go.mod
go.mod是Go语言中用于管理包依赖的工具,它是Go语言官方推荐的依赖管理方式。
go.mod文件是一个文本文件,其中包含了项目所依赖的其他包的信息,包括包名、版本号、源码地址 等信息。项目开发者可以通过go mod命令来管理这些依赖。
Go.mod文件是项目根目录下的go.mod文件,其中记录了项目依赖的其他包的信息。
使用go mod能够解决版本冲突,保证项目稳定运行。同时也可以使得项目更容易维护,对于更新包依赖等操作变得更加方便。
下面是go.mod中一些依赖标识的格式
语义化版本:${MAJOR}.${MINOR}.${PATCH}
如 v1.3.0、v2.3.0等
基于commit的伪版本:vX.0.0-yyyymmddhhmmss-abcdefgh1234
如 v1.0.0-20220401081311-c38fb59326b7
// indirect标识表示间接依赖
主版本2+模块会在模块路径后增加 /vN后缀
没有go.mod文件且主版本2+的依赖,会+incompatible
2) proxy
proxy是指在从远程仓库下载依赖包时,使用一个中间层来帮助下载的工具。
通过使用proxy来进行依赖分发可以保证构建稳定性、依赖可用性和减少第三方压力。
3) go get & go mod
常用的go get命令包括:
- @update: 默认
- @none 删除依赖
- @v1.1.2 语义版本
- @23dfdd5 commit伪版本
- @master 分支最新commit
常用的go mod命令包括:
- init: 创建一个新的go.mod文件
- download: 下载项目依赖的包
- edit: 编辑go.mod文件
- tidy: 维护go.mod文件
三、测试
Go语言中的测试是指使用Go语言编写的测试用例来验证代码是否符合预期。
Go语言中的测试主要由两部分组成:测试用例和测试运行器。
-
测试用例: 测试用例是Go语言编写的代码,用于测试其它代码。测试用例文件必须以_test.go结尾。
-
测试运行器: 测试运行器是Go语言提供的工具,用于运行测试用例。通常使用go test命令来运行测试用例。
Go语言的测试用例支持多种测试方式,常用的有:
- 单元测试: 用于测试程序中的单个函数或方法
- 基准测试: 用于测试程序中的性能
下面是一个Mock测试和基准测试的例子
import (
"bufio"
"os"
"strings"
"testing"
"bou.ke/monkey"
"github.com/stretchr/testify/assert"
)
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 TestProcessFirstLineWithMock(t *testing.T) {
monkey.Patch(ReadFirstLine, func() string { return "line110" })
defer monkey.Unpatch(ReadFirstLine)
line := ProcessFirstLine()
assert.Equal(t, "line010", line)
}
在这个例子中,ReadFirstLine函数用于从文件中读取文件第一行的数据,ProcessFirstLine函数把第一行数据中包含'11'的字符串替换为'00'。
在mock测试中,测试用例屏蔽了外部依赖使ReadFirstLine无需读取文件而可以返回字符串'line110',然后通过断言来判断ProcessFirstLine函数是否正确执行
下面是基准测试
import (
"github.com/bytedance/gopkg/lang/fastrand"
)
var serverIndex [10]int
func InitSeverIndex() {
for i := 0; i < 10; i++ {
serverIndex[i] = i + 100
}
}
func Select()int {
return serverIndex[fastrand.Intn(10)]
//return serverIndex[rand.Intn(10)]
}
func BenchmarkSelect(b *testing.B) {
InitSeverIndex()
b.ResetTimer()
for i := 0; i < b.N; i++ {
Select()
}
}
func BenchmarkSelectParallel(b *testing.B) {
InitSeverIndex()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Select()
}
})
}
在基准测试中,用到了字节开发的fastrand,对性能进行测试,下面是在我的环境中的rand和fastrand的基准测试结果,可以很明显的看出性能的提升
rand
fastrand
四、项目实战
1. 项目背景
获取青训营的主题帖和相应的帖子回复
2. 需求描述
要能够展示主题帖的信息和回帖列表及回帖的信息
3. ER图
4. 项目结构分层
- 数据层,对外部数据的CURD
- 逻辑层,处理业务逻辑
- 视图层,处理与前端的交互逻辑
5. 工具框架选择
Gin - github.com/gin-gonic/g…
6. 编码 & 结果展示
五、今日总结
今天学习了Go语言的并发编程,依赖管理,测试和项目实战。学习了Go语言中的goroutine和channel,以及如何使用它们来实现并发编程。同时还学习了如何使用第三方包管理工具管理项目依赖,以及如何使用Go语言自带的测试工具进行单元测试。最后,还学习了如何使用gin框架构建一个简单的Web应用程序。
明天继续加油