这是我参与「第五届青训营 」笔记创作活动的第2天。
一、前言
今天的课程是由赵老师讲解的Go语言的进阶内容,包括并发编程、依赖管理、单元测试,这些都是项目实战中必备的技能树,要想成为Go语言的后端程序员,这些知识必不可少,并发编程提高我们实现的服务器的用户请求吞吐量,依赖管理使我们站在巨人的肩膀上开发,并且保证了各个不同的依赖包之间的不冲突,单元测试是开发过程中巩固系统稳定性,减少bug产生的重要一环。此外,老师还结合一个实际的应用场景,演示了工程中从需求到设计到代码到测试的一套流程,帮助我更快地理解工程开发,为后续的大项目打好了基础。
二、语言进阶笔记
并发编程
语言背景
Go语言的最大的一个特点就是快,那么为什么会快呢?
- 并发:多线程程序在一个核的cpu上运行
- 并行:多线程程序在多个核的cpu上运行
- Go可以充分发挥多核优势,高效运行
协程与线程
- 协程: 用户态,轻量级线程,栈KB级别。运行在用户态,由Go语言管理
- 线程: 内核态,线程跑多个协程,栈MB级别,运行在内核态,由操作系统管理
Goroutine
Goroutine的使用示例
package main
import (
"fmt"
"time"
)
func main() {
HelloGoRoutine()
time.Sleep(time.Second)
}
func HelloGoRoutine() {
for i := 0; i < 5; i++ {
go hello(i)
}
}
func hello(i int) {
fmt.Println("hello goroutine :", i)
}
channel
** CSP (Communicating Sequential Processes)**
** Channel **
- 创建:make(chan 元素类型,[缓冲大小])
- 有缓冲大小则是有缓冲通道,否则是无缓冲通道(阻塞通道)
- 示例代码
package main
func main() {
CalSquare()
}
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)
}
}
/* 输出
0
1
4
9
16
25
36
49
64
81
*/
并发安全包sync
Mutex
- 在sync包下的Mutex结构体
- 该结构体的私有方法Lock()和Unlock()可以做到加锁解锁保证并发安全
WaitGroup
- 本质是一个计数器,能保证并发安全
- Add(delta)方法:计数器加delta
- Done():计数器-1
- Wait()阻塞协程直到计数器为0
依赖管理
学会站在巨人的肩膀上,有很多现成的代码包,我们在项目代码中可以引入这些代码包,形成依赖。但是代码包可能会有更新与改进,导致版本不统一的问题,因此Go中有以一个依赖管理的演进。
GOPATH阶段
- 环境变量,为Go开发时的工作区
- 在这个目录中存在项目编译的二进制文件,项目编译的中间产物,以及最重要的src项目源码目录
- 项目代码直接依赖 src下的代码
- go get 下载最新版本的包到 src 目录下
- 问题 AB两个不同的项目依赖不同的其他代码包的版本,那么在我们的项目中会出现版本冲突
Go Vendor
- 项目目录下增加 vendor 文件,所有依赖包副本形式放在 $ProjectRoot/vendor
- 依赖寻址方式: vendor => GOPATH。优先vendor再去找GOPATH的依赖包
- 通过每个项目引入一份依赖的副本,解决了多个项目需要同一个 package依赖的冲突问题。
- 问题 同一项目中不同依赖包依赖到不同的版本的其他依赖包同样会出现版本冲突的问题
Go Module
-
通过 go.mod 文件管理依赖包版本
-
通过 go get/go mod 指令工具管理依赖包
-
依赖管理三要素
- 配置文件,描述依赖
go.mod - 中心仓库管理依赖库
Proxy - 本地工具
go get/mod
- 配置文件,描述依赖
-
依赖配置文件
- 依赖标识: [Module Path] [Version/Pseudo-version]。两种版本
- 语义化版本:{MAJOR].{MINOR].${PATCH} eg:V1.3.0 / V2.3.0
- 基于 commit 伪版本:vX.0.0-yyyymmddhhmmss-哈希校验码
- indirect: 间接依赖,代码中没有直接使用但被间接使用的包
- incompatible:对于没有 go.mod 文件并且主版本2+的依赖,会+incompatible
-
依赖分发-回源
- 早期的依赖包直接通过版本管理工具(Git,SVN等)下载依赖包代码时会出现问题
- 无法保证构建稳定性增加/修改/删除软件版本
- 无法保证依赖可用性删除软件
- 增加第三方压力代码托管平台负载问题
- 依赖分发-Proxy
- 工具 go get
+
go get example.org/pkg @update默认,拉取major最新的提交go get example.org/pkg @none删除依赖go get example.org/pkg @v1.1.2tag版本,语义版本go get example.org/pkg @23dfdd5特定的commitgo get example.org/pkg @master分支的最新commit
- 工具 go mod
go mod init:初始化,创建go.mod文件go mod download:下载模块到本地缓存go mod tidy:增加需要的依赖,删除不需要的依赖
- 早期的依赖包直接通过版本管理工具(Git,SVN等)下载依赖包代码时会出现问题
测试
- 测试是避免事故发生的最后一个屏障。
- 测试分为三种类型:回归测试、集成测试、单元测试
- 回归测试:验收阶段,直接使用功能,如刷抖音,评论等
- 集成测试:对系统功能维度做测试,自动化,测试对外接口
- 单元测试:面对开发阶段,开发者对功能模块测试
- 覆盖率逐渐变大,成本组逐渐降低
单元测试
- 输入测试案例,经过单元测试得到输出,与期望的输出进行校对。
- 单元测试能提高效率,在较短的时间内找到问题,保证质量,确保功能正常
- 单元测试规则
- 结尾:测试文件以_test.go结尾
- 函数名:funcTestXxx(*tesing.T)
- 初始化逻辑放到TestMain中
- 覆盖率:
- 衡量代码是否经过了足够的测试
- 评价项目的测试水准
- 评估项目是否达到了高水准测试等级
- 一般覆盖率 : 50%~60%,较高覆盖率80%+,
- 测试分支相互独立、全面覆盖。
- 测试单元粒度足够小,函数单一职责
Mock测试
- 外部依赖需要幂等与稳定,确保每次运行的结果是一样的,能在任何时间、任何地点
- 但是数据库,文件存储这些可能发生变化,会出现不幂等,使用Mock
- Mock能为函数打桩,在实际测试中运行的是我们另外自定义的测试方法进行方法替换,不再依赖文件和数据库
基准测试
- 优化代码,需要对当前代码分析。内置的测试框架提供了基准测试的能力
- 函数命名BenchmarkXxx(b *tesing.B)
- b.ResetTimer()重置计时器
- b.RunParaller(func)并行测试
三、项目实战
需求
- 需求:社区话题页面需要展示话题(标题,文字描述)和回帖列表。需要后端给前端提供话题
- 暂不考虑前端页面实现,仅仅实现一个本地web服务,话题和回帖数据用文件存储
代码设计
- 需求用例:用户浏览话题页面,包括话题数据和回帖数据
- ER图:
- Topic:(id,title,content,create_time)
- Post:(id,topic_id, content.create_time)
- Topic和Post的关系是一对多的关系
- 分层结构
- 数据层: 数据 Model,外部数据的增删改查
- 逻辑层: 业务 Entity,处理核心业务逻辑输出
- 视图层: 视图 view,处理和外部的交互逻辑
- 组件工具
- 使用Gin高性能 go web 框架:GitHub地址
- God Mod:
go mod init & go get gopkg.in/gin-gonic/gin.v1@v1.3.0
- Repository-index
- 使用索引来缓存数据,并使用哈希来加快查找
- Service
- 检验参数
- 准备数据
- 打包数据
- 返回页面数据
- Controller
- 解析参数,类型转换
- 封装打包返回实体数据
- 返回实体数据
- Router
- 初始化数据索引
- 初始化引擎配置
- 构建路由
- 启动服务
func main() {
if err := Init(); err != nil {
os.Exit(-1)
}
r := gin.Default()
r.Use(gin.Logger())
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.GET("/community/page/get/:id", func(c *gin.Context) {
topicId := c.Param("id")
data := handler.QueryPageInfo(topicId)
c.JSON(200, data)
})
r.POST("/community/post/do", func(c *gin.Context) {
uid, _ := c.GetPostForm("uid")
topicId, _ := c.GetPostForm("topic_id")
content, _ := c.GetPostForm("content")
data := handler.PublishPost(uid, topicId, content)
c.JSON(200, data)
})
err := r.Run()
if err != nil {
return
}
}
- 运行测试
go run server.go
四、课后个人总结
本次课程的知识点都非常重要,要想实现高质量的服务器,并发知识能带来质的飞跃,并发协程,并发安全包是确保业务逻辑一致性的重要工具。测试也十分重要,测试是保证代码质量,确保功能正常的必不可少的一个环节。最后老师好结合一个实际的案例带来讲解,使我了解了大致的开发和实现流程。