这是我参与「第三届青训营 -后端场」笔记创作活动的的第3篇笔记
Go工程实践
语言进阶
并发 vs 并行
并行是实际的多个核上同时运行程序
Go 可以充分发挥多核优势,高效运行
Goroutine
协程: 用户态,轻量级线程, 其调度由 go 本身负责, 栈在 kb 级别
线程: 内核态,一个线程跑多个协程,栈在 mb 级别
CSP ( Communicating Sequential Process )
sharing memory by communication , 而不是通过共享内存而实现通信
Channel
管道通道 make 创建
以其内部是否有缓冲, 可以将管道划分为 无缓冲通道 和 有缓冲通道:
- 无缓冲通道,又称 "同步通道",有数据时即会阻塞
并发安全 Lock
sync 包下面很多工具用于并发同步控制,muteX 就是常用锁
WaitGroup
用于协程同步,其原理是维护一个计数器
通过 Add、 Done 、 Wait 三个方法执行功能
依赖管理
依赖的 思想: 站在巨人肩膀上
依赖管理演进
- GOPATH
- Go Vendor
- Go Module
解决的问题:
- 不同项目(环境)需要依赖的版本不同
- 控制依赖库的版本
GOPATH
是 Go 语言控制的环境变量,是 Go 默认的工作区
存放三个文件夹:
- bin :项目编译的二进制文件
- pkg:项目编译的中间产物,加速编译
- src:存放项目源码
缺点:
-
无法实现 package 的多版本控制
不同项目可能依赖同一个 package 的不同版本,然而 src 只有一份
Go Vendor
在项目的目录下,增加一个 Vendor 文件,所有依赖包副本形式放在其中
在编译时,现在 Vendor 下寻找所使用的 package ,找不到再从 GOPATH 中寻找
缺点:
- 无法控制依赖的版本
- 更新项目又可能出现依赖冲突,导致编译出错
归根结底,其本质问题是: 依赖的源码文件
Go Module
通过 go.mod 文件管理依赖包版本
通过 go get / mod 指令工具管理依赖包
终极目标:定义版本规则和管理项目依赖关系
依赖管理三要素
- 配置文件,描述依赖 go.mod
- 中心仓库管理依赖库 Proxy
- 本地工具 go get / mod
go.mod
内容可以分为三部分:
-
依赖管理基本单元
-
原生库
-
单元依赖
- 每个依赖分为 path 和 版本两部分
version
有两个方式:
major 版本之间可以是不兼容的,minor 通常是兼容的,小版本修改; 而 patch 通常是 bug 修复
commit 伪版本 前面是语义化一样的,中间是时间戳,最后是哈希校验码
indirect
没有直接导入的 module,会有这个标记
依赖分发—回源
依赖去哪里下载, 如何下载
缺点:
-
无法保证构建稳定性
- 增加、修改、删除软件版本
-
无法保证依赖可用性
- 删除软件
-
增加第三方压力
- 代码托管平台负载问题
依赖分发—Proxy
是一个服务站点,会缓存软件内容,保证稳定性
依赖分发—变量 GOPROXY
服务站点 URL 列表,direct 代表前面的服务站点找不到的话,会回到 源代码托管站点 找
工具—go get
工具—go mod
tidy 经常用,对 go mod 里的依赖重新检查,减少构建时间
测试
质量就是 “安全”
稳定性重中之重
营业事故一览
测试是避免事故的发生
测试初识
回归测试:如 QA同学在场景下执行测试
集成测试:自动化地对于功能、接口进行测试
单元测试:开发者对单独模块进行测试
因此单元测试一定程度上决定了软件的质量
单元测试
单元测试—规则
-
所有测试文件以
_test.go结尾 -
测试函数以
Test开头 -
初始化逻辑放在
TestMain中执行testing.M调用Run方法执行所有的测试函数
单元测试—覆盖率
如何衡量代码是否经过了足够的测试?
如何评价项目的测试水准?
如何评估项目是否达到了高水平测试等级?
一个重要指标就是:代码覆盖率
在 go test 指令中,后面加上一个 --cover 可以在输出的时候
单元测试—Tips
单元测试—依赖
单元测试需要达到:
- 幂等 : 即多次运行得到的结果是一致的
- 稳定: 单元测试是相互隔离的
由于运行中对于文件和 cache 、数据库等的依赖,为了单元测试的两个要求,引入了 mock 机制
mock
mock 的组件有很多,这里用到的是一个开源的包:github.com/bouk/monkey
实现了对 method 对实例的方法进行打桩
打桩:
- 为一个函数打桩
- 为一个方法打桩
主要为下面这两个函数!
本质是使用 unsafe 包,运行时将内存中函数地址 替换成 运行时函数地址
基准测试
- 优化代码,需要对当前代码分析
- 内置的测试框架提供了基准测试的能力
基准测试—运行
基准测试的函数名以 Benchmark 开头
提供了 ResetTimer 等方法避免不相关代码的影响
项目实战
项目流程主要包括:
- 需求设计
- 代码开发
- 测试运行
需求描述
需求用例
用户交互的主要对象: 话题 和 回帖
这两个对象应该如何存储呢?
ER 图
Topic 和 Post 是一对多的关系
分层结构
将代码分成三个部分
数据层主要面对逻辑层,对逻辑层是透明的——逻辑层不关心具体底层数据的数据存储,只关心有无数据
逻辑层负责: 接收数据层数据,做打包封装,输出一个 Entity,这里就是对应话题页面
视图层对上层负责,对数据做一些封装,可以以一些 API 的形式访问
组件工具
存储层实现
Topic 和 Post 是两个结构体,字段定义如下:
// topic.go
type Topic struct {
Id int64 `gorm:"column:id"`
UserId int64 `gorm:"column:user_id"`
Title string `gorm:"column:title"`
Content string `gorm:"column:content"`
CreateTime time.Time `gorm:"column:create_time"`
}
// post.go
type Post struct {
Id int64 `gorm:"column:id"`
ParentId int64 `gorm:"parent_id"`
UserId int64 `gorm:"column:user_id"`
Content string `gorm:"column:content"`
DiggCount int32 `gorm:"column:digg_count"`
CreateTime time.Time `gorm:"column:create_time"`
}
这些都是存储在本地文件中的,因此是以 json 序列化的形式存储在本地
此时需要两个查询:
- 从 话题 id 找到 话题
- 从 话题 id 找到关联的所有 post
存储库索引
类似数据库 索引 与 元数据 的关系,通过 map 来构建对应关系
数据文件是通过将结果体序列存储到本地文件里
通过扫描文件里的所有行然后读取内容出来
然后通过单例模型生成两个实例,包装这两个 map, 并提供函数方法用于查询
服务层实现
结构体 PageInfo 这个实体用于保存从文件中获取的数据
type PageInfo struct {
Topic *repository.Topic
PostList []*repository.Post
}
整个服务层的逻辑:
- 参数校验:进行非法校验
- 准备数据:
- 组装实体
Controller 层
构建 View 对象
- Code :用于标识是否读取成功
- Msg: 具体错误信息 或 表示成功的信息
- Data: 数据实体
Router
在上面已经把基本框架实现完成了
下面就是通过 gin 搭建 web 框架
流程:
构建路由,这部分不太懂
最终运行 server.go 文件后,就可以通过网络请求获取到数据,前端根据获取到的数据来渲染页面