这是我参与「第五届青训营 」的第 2 天
今天课程主要内容是Go语言进阶-工程实践:
1. 语言进阶
2. 依赖管理
3. 测试
4. 项目实战
注:笔记图片来自课程截图,如有侵权,请联系删除
1.语言进阶
Goroutine
Go语言的特点是高性能、高并发,可以充分发挥多核优势,高效运行。主要原因在于Go语言引入了Goroutine
(协程)的概念。Goroutine
是轻量级的纯用户态线程,一个线程能够跑多个协程。在课程案例中,对于主协程需要进入休眠态time.Sleep(time.Second)
,以防止子协程还未结束时主协程已经结束。
CSP (Communicating Sequential Processes)
协程之间提倡使用通道共享内存而不是通过共享内存实现通信。通过共享内存会存在临界区以及数据静态(不能修改的数据)。
Channel
channel
通道通过make
关键字进行创建,及make(chan 元素类型, [缓冲大小])
这一语法。其中缓冲大小是可选项,定义了缓冲大小的是有缓冲通道,无缓冲通道则是同步通信。
并发安全 Lock
引用sync.Mutex
类型,对于没上锁的协程存在并发安全的问题,例如课例中对某一个变量执行2000次+1操作,5个协程并发执行不上锁变量的值将不确定,但是上锁变量能够在原先的值+2000。要注意Lock()
完以后需要Unlock()
,主协程需要进入睡眠。
WaitGroup
因为不知道子协程会在什么时候运行,主协程阻塞时长不好掌控,因此Go在Sync包里提供了WaitGroup来实现并发的同步。包含三个方法Add(delta int)
用于协程开启时计数delta,Done()
计数减1常用defer来调用,Wait()
表示主协程阻塞到计数器为0。
2.依赖管理
对于hello word 单体函数只需要依赖原生SDK,但是实际工程可能涉及framework、collection、log、driver、collection等一系列依赖会通过sdk方式引入,因此需要对依赖包进行管理。
Go依赖管理演进
经历3个阶段GOPATH -> Go Vendor -> Go Module。主要围绕实现两个目标:不同环境依赖的版本不同以及控制依赖库的版本。
GOPATH是指Go语言支持的环境变量,值是Go项目的工作区,目录结构包括src存放源码,pkg编译中间产物,bin存放Go项目生成的二进制文件。但是GOPATH存在弊端,当两个项目依赖某一package的不同版本,就没办法实现package的多版本控制。
Go Vendor出现解决了上述问题,即工作目录下有个Vendor目录存放依赖的副本,如果依赖存在于Vendor会优先使用该目录的依赖,否则从GOPATH寻找。但是存在无法控制依赖版本以及会出现依赖冲突导致编译出错的问题。
Go Module是Go语言官方推出的依赖管理系统,解决了无法依赖同一库多个版本等问题。通过go.mod文件管理依赖包版本,以及go get/go mod
指令工具管理依赖包,实现了定义版本规则和项目依赖关系。
依赖管理三要素
- 配置文件描述依赖 go.mod
- 中心仓库管理依赖库 Proxy
- 本地工具 go get/mod
依赖配置go.mod如下:
go mod定义了版本语义规则,分为语义化版本和基于commit伪版本如下:
MAJOR版本表示不兼容的API,即使同一个库,也会被认为是不同的包。MINOR版本通常是新增函数或者功能,向后兼容。patch一般是进行了Bug修复的版本。基于commit伪版本,基础版本前缀和语义版本一致,时间戳表示提交时间,最后校验码是12位哈希前缀,每次commit后Go会默认生成一个伪版本号。
特殊标识indirect
表示间接依赖 A->B->C则A间接依赖C。incompatible
:主版本在2+模块会在模块路径上增加/vN的后缀,能够以按照是不同模块来处理同一项目不同主版本的依赖。但是在gomod之前,有一些仓库就已经是2或者更高版本tag,为了兼容,因此对于没有go.mod的文件并且主版本在2或以上的依赖,会打上该后缀。
依赖分发Proxy如下:
主要为了保证软件版本在增删改查的时候依旧能够正确依赖(构建稳定性和依赖可用性),并且减少代码托管平台负载问题,不用存多个版本。Proxy是服务站点,会缓存原站中的软件内容,版本不会改变,源站删除依然可用。
Go Module通过GOPROCY环境变量控制Proxy,GOPROXY是一个url列表用direct表示源站。
go get工具:
3.测试
测试分为回归测试,集成测试,单元测试,从上到下覆盖率增大,成本在降低。回归是通过终端回归固定主流程场景,集成是对系统功能测试,但愿是都单独函数模块验证。
单元测试
单元测试文件_test.go结尾,测试函数固定格式TestXxx(*testing.T)
,初始化逻辑放在TestMain。运行指令:go test [flags] [packages]
。开源assert能够帮助减少测试代码量。单元测试通过覆盖率衡量是否经过了足够测试。
在对文件进行测试的时候,如果文件不存在对应的内容可能报错,可以通过Mock来进行测试,开源Mock库github.com/bouk/monkey进行打桩测试,不再依赖本地文件。
基准测试
函数BenchmarkXxx(b *testing.B)
b中的N值是反复递增循环测试,举了多个服务器随机选择的例子,并且进行了多协程并发测试,如果在测试前做了init或其他准备操作,注意使用b.ResetTimer()
排除不在测试范围的时间花费。随机选择时为优化,开源了fastrand。
cmd命令go test -bench
package benchmark
import (
"testing"
)
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()
}
})
}
func BenchmarkFastSelectParallel(b *testing.B) {
InitServerIndex()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
FastSelect()
}
})
}
4.项目实践
需求设计
社区话题页面,展示话题和回帖列表,用文件存储,不考虑前端页面,实现本地web服务。话题与帖子关系用了ER图分析。
分层结构
分为数据层进行外部数据增删改查,逻辑层处理核心业务逻辑输出,视图层处理外部交互:
组件工具
利用Gin高性能go web框架以及Go Mod。
package main
import (
"github.com/Moonlight-Zhao/go-project-example/cotroller"
"github.com/Moonlight-Zhao/go-project-example/repository"
"gopkg.in/gin-gonic/gin.v1"
"os"
)
func main() {
if err := Init("./data/"); err != nil {
os.Exit(-1)
}
r := gin.Default()
r.GET("/community/page/get/:id", func(c *gin.Context) {
topicId := c.Param("id")
data := cotroller.QueryPageInfo(topicId)
c.JSON(200, data)
})
err := r.Run()
if err != nil {
return
}
}
func Init(filePath string) error {
if err := repository.Init(filePath); err != nil {
return err
}
return nil
}
然后是各个结构的设计,最后cmd指令go run server.go
本地启动web服务,通过curl --location --request GET 'http://0.0.0.0:8080/community/page/get/2'
请求服务器给出接口,其中需要进行完备单元测试快速定位问题。
总结
学到了很多,对于Go语言协程并发以及并发安全还有单元测试、基准测试有了理解。也学会了Go语言项目的依赖管理。