这是我参与「第三届青训营 -后端场」笔记创作活动的第 5 篇笔记。
本文分享Go语言的上手进阶以及工程实践
01.语言进阶
从并发编程的视角带大家了解Go高性能的本质
01.并发VS并行
并发:多线程在一个核的CPU上运行;并行:多线程程序在多个核的CPU上运行 Go可以充分发挥多核优势,高效运行。Go语言可以说是为高并发而生的。
1.1 Goroutine
协程:用户态,轻量级线程,栈KB级别。
线程:内核态,线程跑多个协程,栈MB级别。
实际开发过程中用go 开发协程
1.2 CSP(Communicating Sqeuential Processes)协程之间的通信
Go语言提倡通过通信共享内存,而不是通过共享内存来实现通信
涉及到一个概念,通道channel
goroutine 是程序并发的执行体
1.3 Channel
Channel 是一种引用类型,创建需要用到make关键字, make(chan 元素类型,[缓冲大小]),根据缓冲大小,可分为有缓冲通道和无缓冲通道
使用无缓冲通道进行通信,会导致发送的goroutine和接收的goroutine同步化,因此无缓冲通道也被称为同步通道。
解决同步问题的一个方式,就是使用带有缓冲区的通道,大小就是通道可存储的元素数量。
带缓冲的channel 能解决生产和消费速度不均衡带来的执行效率问题
1.4 并发安全 Lock
通过lock获取临界区资源,计算完成后再将临界区释放掉 func Add ,不加锁输出不一定为10000,加锁一定为10000,结论:不加锁输出未知结果,这就是并发安全问题,属于 undefined 行为。
如何解决,就是加锁!,通过对临界区权限的控制,来保证并发安全。 并发安全问题往往难定位。
1.5 WaitGroup
前面的例子都用sleep实现暴力的阻塞,这肯定不优雅, Go语言可以通过waitgroup来实现并发任务的同步,
三个方法:Add(delta int) 计数器+delta ; Done() 计数器-1 ; Wait()阻塞知道计数器为0 内部就是维护一个计数器,例如启动了n个并发任务,可add(n), 每个任务完成时,调用Done方法将计数器-1,最后,调用wait方法来阻塞,等待所有的并发任务执行完。 当计数器值为0时,就代表了所有的并发任务已经完成。
小结
Goroutine:go可以通过高效的调度模型来实现协程的一个高并发的操作
Channel:通过通信实现共享内存
Sync:关键字mutex,waitgroup,主要是为了实现并发安全操作和协程的同步
02.依赖 管理
了解Go语言依赖管理的演进路线, 背景|Go依赖管理演进|Go Module实践
hello 用原生的SDK就可以 SDK:Software Development Kit 软件开发工具包
实际开发工程会相对复杂,不可能基于标准库,从0-1编码搭建,我们要把精力放在业务逻辑上,
2.1 Go 依赖管理演进
GOPATH、Go Vendor、Go Module
围绕两个关键:1. 不同环境(项目)依赖的版本不同; 2.控制依赖库的版本
2.1.1 GOPATH
GOPATH Go语言的环境变量,是一个go项目的工作区
bin 项目编译的二进制文件 pkg 项目编译的中间产物,加速编译 src 项目源码
项目代码直接依赖 src下的代码
go get 下载最新版本的包到src目录下
GOPATH 的弊端 project A 和 B 依赖与某一 package 的不同版本。 问题:无法实现package 的多版本控制
2.1.2 Go Vender 项目目录下增加了vender文件,所有依赖包副本形式放在ProjectRoot/vendor 依赖寻址方式:vendor-> GOPATH
通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题
问题,project A 依赖 B 和 C,B依赖D的v1版本,C依赖D的v2版本,无法很好地控制v1和v2版本的选择问题。 可能出现冲突,导致编译错误
问题:
- 无法控制以来的版本
- 更新项目又可能出现依赖冲突,导致编译错误。
2.1.3 Go Module (Go语言推出的依赖管理系统)
- 通过 go.mod 文件管理依赖包版本
- 通过go get/go mod 指令工具管理依赖包
终极目标:定义版本规则和管理项目依赖关系
2.2 依赖管理三要素
- 配置文件,描述依赖 go..mod
- 中心仓库管理依赖库 goproxy
- 本地工具 go get/mod
2.3.1 依赖配置 go.mode中第一行为 依赖管理基本单元 第三行为go原生库版本 之后为单元依赖,每个依赖由两部分组成,一是mod path,后面跟版本号,这样就可以唯一定位某个仓库的某个版本 依赖标识:Module Path
2.3.2
2.3.3 依赖配置-indirect
indirect代表间接依赖 比如:a ->b -> c,a 对 b 就是直接依赖,a对c就是间接依赖
2.3.4 依赖配置-incompatible
- 主版本2+模块会在模块路径增加/vN 后缀
- 对于没有go.mod 文件并且朱版本2+的依赖,会+incompatible
2.3.4 依赖配置-依赖图
2.3.5 依赖分发-回源
- 无法保证构建稳定性 增加、修改、删除软件版本
- 无法保证依赖可用性 删除软件
- 增加第三方压力 代码托管平台负载问题
goproxy 是一个服务站点,会缓存原站中的软件内容
2.3.7 工具 - go get
go get example.org/pkg @update 默认 @none 删除依赖 @v1.1.2 tag版本,语义版本 @23dfdd5 特定的commit @master 分支的最新commit
go mod
- init 初始化,构建go.mod 文件
- download 下载模块到本地缓存
- tidy 增加需要的依赖,删除不需要的依赖
小结
- Go依赖管理演进
- Go Module 依赖管理方案
03.测试
从单元测试实践除法,提升大家的质量意识 单元测试|Mock测试|基准测试
事故:
- 营销配置错误,导致非预期用户享受权益,资金损失10W+
- 用户体现,幂等失效,短时间可多次体现,资金损失20W+
- 代码逻辑错误,广告位被占,无法出广告,收入损失500W+
- 代码指针使用错误,导致APP不可用,损失上KW+
测试是避免事故的最后一道屏障
回归测试,集成测试,单元测试 从上到下,覆盖率逐层变大,成本却逐层降低
单元测试主要包括,输入,测试单元,输出,以及校对,单元的概念比较广,包括接口,函数,模块等;用最后的校对来保证代码的功能与我们的预期相符;单侧一方面可以保证质量,在整体覆盖率足够的情况下,一定程度上既保证了新功能本身的正确性,又未破坏原有代码的正确性。另一方面可以提升效率,在代码有bug的情况下,通过编写单测,可以在一个较短周期内定位和修复问题。
3.1.1 单元测试-规则
3.1.5 单元测试-覆盖率
go test judgment_test.go judgment.go --cover 得到覆盖率,被测试函数运行的行数百分比
- 一般覆盖率:50%~60%,较高覆盖率80%+
- 测试分支相互独立、全面覆盖
- 测试单元粒度足够小,函数单一职责
3.2 单元测试-依赖
幂等是指每一次测试运行都应该产生与之前一样的结构,而要实现这一目的就要用到mock机制。
3.3 单元测试-文件处理
3.5 基准测试
- 优化代码,需要对当前代码分析
- 内置的测试框架提供了基准测试的能力
举个服务器负载均衡的例子
小结
单元测试 Mock测试 基准测试
04.项目实战
通过项目需求、需求拆解、逻辑设计、代码实现带领大家感受下真实的项目开发 需求设计|代码开发|测试运行
需求背景:掘金的社区话题入口,页面的功能包括话题详情、回帖列表、支持回帖、点赞和回帖回复,我们今天就以此为需求模型,开发一个该页面交涉
社区话题页面
- 展示话题(标题,文字描述)和回帖列表
- 暂不考虑前端页面实现,仅仅实现一个本地web服务
- 话题和回帖数据用文件存储
4.4 分层结构3
- 数据层:数据Model,外部数据的增删查改
- 逻辑层:业务Entity,处理核心业务逻辑输出
- 视图层:视图view,处理和外部的交互逻辑
整体分为三层,repository数据层,service逻辑层,controoler视图层, 数据层关联底层数据模型,也就是这里的model,封装外部数据的增删改查,我们的数据存储在本地文件,通过文件操作拉取话题,帖子数据;数据层面向逻辑层,对service层透明,屏蔽下游数据差异,也就是不管下游是文件,还是数据库,还是微服务等,对service层的接口模型是不变的。 Servcie逻辑层处理核心业务逻辑,计算打包业务实体entiy,对应我们的需求,就是话题页面,包括话题和回帖列表,并上送给视图层; Controller视图层负责处理和外部的交互逻辑,以view视图的形式返回给客户端,对于我们需求,我们封装json格式化的请求结果,api形式访问就好。
4.5 组件工具3
Gin 高性能 go web 框架
4.6 Repository-查询3
实现索引后就是实现查询操作。就比较简单了,根据索引直接查询即可。利用syc.Once,sync.Once 适合在高并发场景下只执行一次的情况,单例模式,可减少存储的浪费。
索引:话题ID,话题ID 数据:话题 帖子列表
有了这两个函数,我们就可以上送给逻辑层,在逻辑层进行实体entity的封装
4.7 Service33
实体:
type PageInfo struct {
Topic *repository.Topic
PostList []*repository.Post
}
流程:参数校验 --> 准备数据 --> 组装实体
query_page_info.go 通过err控制整个流程的退出,正常会返回页面信息,err为nil
checkParam 做数据校验 prepareInfo,通过respos那一层获取话题数据和回帖列表数据,两者对话题信息和回帖信息并行处理,可提高执行效率(话题信息和回帖信息两者是没有相互依赖的,对这种情况,就可以考虑并行处理)
4.8 Controller
Service层处理完成,下面就是Controller层,这一层的逻辑是比较简单的
- 构建View对象
- 业务错误码
首先,构建view对象,也就是 PageData
至此,从repository到Service到Controller,整个代码结构、框架已经实现完成,业务逻辑整体完成
4.9Router
通过通过gin搭建整个web框架,分四步:
- 初始化数据索引
- 初始化引擎配置
- 构建路由
- 启动服务
最后执行 go run 本地启动 web 服务,通过 curl 命令请求服务暴露的接口