这是我参与「第五届青训营」伴学笔记创作活动的第 2 天
一、本堂课重点内容
本节课程主要分为四个方面:
- 并发编程:包括协程、通道、锁、线程同步
- 依赖管理:Gopath, Go Vendor, Go Module
- 单元测试
- 项目实战
二、详细知识点介绍
1. 并发编程
-
Goroutine
-
协程与线程
- 线程:内核态、一个线程有多个协程、在栈上为mb级别。
- 协程:用户态、轻量级线程、在栈上为kb级别。
-
开启一个协程
-
使用匿名函数创建goroutine
go func( 参数列表 ){ 函数体 }( 调用参数列表 ) -
为一个普通函数创建goroutine
go 函数名( 参数列表 )
-
-
CSP (Communicating Sequential Processes)并发模型:提倡通过通信共享内存而不是通过共享内存实现通信。
-
-
Channel
-
定义一个通道:
make(chan 元素类型,[缓冲大小]) -
可以定义无缓冲通道,可以保证发送和接收协程的同步;定义带缓冲通道,可以解决消费者消费速度比生产者的生产速度慢的问题。
-
eg. channel的应用:通过sec和dest的传递保证了输出的顺序性
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) } }
-
-
Lock:保证并发安全
- 锁的定义:
var lock sync.Mutex - 加锁和解锁:
lock.Lock()/lock.Unlock()
- 锁的定义:
-
WaitGroup:实现并发任务的同步
-
WaitGroup的定义:
var wg sync.WaitGroup -
eg. 等待五个协程
func ManyGoWait() { 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() }
-
2. 依赖管理
-
依赖管理演进:GOPATH -> Go Vender ->Go Module
-
GOPATH
-
GOPATH是go语言的环境变量,它是go项目的工作区。
-
目录下内容:
-
项目代码直接依赖src下代码。
-
使用
go get下载最新版本的包在src目录下。 -
弊端:无法实现package的多版本控制。
-
-
Go Vender
- 项目目录下增加vendor文件,所有依赖包的副本放在
$ProjectRoot/vendor目录下。 - 通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题。
- 弊端:无法控制依赖的版本、更新项目可能出现依赖冲突。
- 项目目录下增加vendor文件,所有依赖包的副本放在
-
Go Module
- 通过
go.mod文件管理依赖包版本。 - 通过
go get/go mod指令工具管理依赖包。
- 通过
-
-
依赖管理三要素
-
配置文件,描述依赖:
go.mod -
中心仓库管理依赖库:
Proxy -
本地工具:
go get/mod
-
-
依赖配置
-
go.mod
依赖标识:[Module Path] [Version/Pseudo-version]
-
version
- 语义化版本:
${MAJOR}.${MINOR}.${PATCH}- MAJOR:大版本,互相可以不兼容
- MINOR:新增功能,版本间一般兼容
- PATCH:代码bug修复
- 基于commit伪版本:
vX.0.0-时间戳-12位hash码
- 语义化版本:
-
indirect
- 对于没有直接导入的间接依赖模块,用indirect关键字标识。
-
incompatible
- 对于没有go.mod文件以及主版本在v2及以上的依赖,加上incompatible后缀,标识可能有兼容问题。
-
依赖图:选择满足本次构建的最低的兼容版本
-
依赖分发
-
go get/mod的使用
-
3. 测试
-
测试的分类:回归测试、集成测试、单元测试。
- 测试成本逐渐降低,覆盖率逐步上升。
- 单元测试的覆盖率决定着代码质量。
-
单元测试
-
规则
-
使用assert函数来判断测试的正确性
-
覆盖率:衡量代码是否已经经过了足够的测试
- 一般来说要达到50%~60%,要求严格的资金型服务需要80%以上。
- 测试单元粒度小可以提升覆盖率。
-
-
mock测试
-
工程中复杂的项目一般会依赖数据库和文件,单元测试需要保证稳定性和幂等性,要实现这一目的需要mock机制。
-
开源的mock测试库:monkey
-
通过patch对函数进行打桩,摆脱对于文件的束缚和依赖。
-
-
基准测试
-
规则
- 所有测试文件以
_test.go结尾。 - 测试函数为
func BenchmarkXxx(b *testing.B)
- 所有测试文件以
-
b.ResetTimer()函数重置计时器。 -
多协程并发测试示例:
func BenchmarkFastSelectParallel(b *testing.B) { InitServerIndex() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { FastSelect() } }) }rand.Intn()函数为了并发安全,使用了全局锁,导致并发情况下的速度劣化。- 高性能随机数方法:
fastrand.Intn(),牺牲一定的数列一致性,提高了效率。
-
三、实践练习例子
-
需求描述:实现社区话题页面,展示话题和回帖列表。不考虑前端实现,仅实现本地web服务。
-
E-R图设计:实体包括话题和贴子,关系为一对多,设计出实体的属性。
-
分层结构:数据层->逻辑层->视图层
- 数据层屏蔽了下游的数据差异,对逻辑层的接口模型不变。
-
基础组件和工具
- gin:go web框架
- go mod:实现依赖管理
-
具体实现
- repository
- 利用索引实现查询(map)。
- 利用结构体封装查询操作,业务逻辑层只需要调用结构体的方法即可完成相应的操作。
- sync.Once类型的.Do操作保证操作的函数在多线程环境下只会被执行一次,这里用来保证程序中只有一个查询结构体的实例存在。
- service
- 参数校验:检查传入的参数是否合法。
- 准备数据:调用repository层提供的接口进行查询,注意其中可以并行执行的流程,降低接口耗时。
- 组装实体:将查询到的数据组装起来并返回。
- controller:调用service层的接口构建view对象,并返回业务错误码。
- 空接口:interface{},可以表示任意类型的变量。
PageData结构体的内容为错误码、错误信息、返回的数据。
- router:通过gin搭建web框架,流程为
- 初始化数据索引:在本例中为构建map索引
- 初始化引擎配置:
r := gin.Default() - 构建路由:使用r的方法。
- 启动服务:服务通过http暴露出去。
- repository
-
问题总结
-
完善环境变量
go env -w GOPROXY="https://goproxy.io"go env -w GO111MODULE="on"- 可以在创建项目时在环境中填写:
https://goproxy.cn,direct
-
配置gin
- 建立mod:
go mod init 文件夹名 - 下载Gin环境依赖(用
-u标记来更新本地的对应的代码包):go get -u github.com/gin-gonic/gin - 继续完善依赖,下载到本地:
go mod download - 增加缺少的module,删除无用的module:
go mod tidy import "github.com/gin-gonic/gin"
- 建立mod:
-
运行并测试
- 启动服务:
go run server.go - 测试:
curl –-location –-request GET http://127.0.0.1:8080/community/page/get/2 | python -m json.tool
- 启动服务:
-