这是我参与「第三届青训营-后端场」笔记创作活动的第二篇笔记。
并发VS并行
并发:多线程程序在一个核的cpu上运行
并行:多线程程序在多个核的cpu上运行
Goroutine
协程:用户态,轻量级线程,栈MB级别
线程:内核态,线程跑多个协程,栈KB级别
Go协程:
go func
CSP
提倡通过通信共享内存而不是通过共享内存而实现通信
Channel
make(chan 元素类型,[缓冲大小]
无缓冲通道 make(chan int)
有缓冲通道 make(chan int,2) 可解决生产和消费速度不均衡的执行效率问题 //defer 延迟资源关闭
并发安全Lock
addWithLock & addWithoutLock
不加锁的输出值不一定为预期结果
WaitGroup
Add(delta int) // 开启协程+1
Done() // 执行结束-1
Wait() // 主协程阻塞直到计数器为0
依赖管理
背景
- 工程项目不可能基于标准库0-1编码搭建
- 管理依赖库
Go依赖管理演进
- 不同环境(项目)依赖的版本不同
- 控制依赖库版本
GOPATH
环境变量$GOPATH
- bin // 项目编译的二进制文件
- pkg // 项目编译的中间产物,加速编译
- src // 项目源码
项目代码直接依赖src下的代码
go get 下载最新版本的包到src目录下
弊端
场景:A和B依赖于某一package的不同版本
问题:无法实现package的多版本控制
Go Vendor
- 增加vender文件,所有依赖包副本形式放在$ProjectRoot/vendor
- 依赖寻址方式:vendor => GOPATH
// 通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题
弊端
问题:
- 无法控制依赖的版本
- 更新项目又可能出现依赖冲突,导致编译出错
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
V1.3.0
V2.3.0
- 基于commit伪版本
vX.0.0-yyyymmddhhmmss-abcdefgh1234
版本号-时间戳-哈希码
依赖配置-indirect
A->B->C
A->B 直接依赖
A->C 间接依赖 // indirect
依赖配置-incompatible
不同MAJOR版本间可以不相互兼容
- 主版本2+模块会在模块路径增加/vN后缀
- 对于没有go.mod文件并且主版本2+的依赖,会+incompatible
依赖配置-依赖图
项目中有依赖不同版本时,选择最低的兼容版本
依赖分发
依赖分发-回源
直接使用版本管理仓库下载依赖的问题:
- 无法保证构建稳定性 增加/修改/删除软件版本
- 无法保证依赖可用性 删除软件
- 增加第三方压力 代码托管平台负载问题
依赖分发-Proxy
在中间缓冲 稳定可靠
依赖分发-GOPROXY
GOPROXY = "proxy1.cn, peoxy2.cn, direct"
服务站点URL列表,"direct"表示源站
工具
工具-go get
go get example.org/pkg
@update 默认
@none 删除依赖
@v1.1.2 tag版本,语义版本
@23dfdd5 特定的commit
@master 分支的最新commit
工具-go mod
go mod
init 初始化,创建go.mod文件
download 下载模块到本地缓存
tidy 增加需要的依赖,删除不需要的依赖
测试
- 回归测试
- 集成测试
- 单元测试
从上到下,覆盖率逐层变大,成本却逐层降低
//单元测试的覆盖率一定程度反映代码的质量
单元测试
单元测试-规则
- 所有测试文件以_test.go结尾
- Func Text(*testing.T)
- 初始化逻辑放到TestMain中
//测试前:数据装载、配置初始化等前置工作
code := m.Run()
//测试后:释放资源等收尾工作
单元测试-运行
go test [flags][packages]
单元测试-assert
eg. "githb.com/strechr/testify/assert"
单元测试-覆盖率
- 如何评估---覆盖率
单元测试-Tips
- 一般覆盖率:50%-60%,较高覆盖率80%+
- 测试分支相互独立、全面覆盖
- 测试单元粒度足够小,函数单一职责
单元测试-依赖
外部依赖 => 稳定&幂等 // 幂等:重复运行函数,结果相同
// 稳定:单元测试在任何时间任何函数可独立运行
单元测试-Mock
Monkey: github.com/bouk/monkey
快速Mock函数
- 为一个函数打桩
- 为一个方法打桩
不再依赖本地文件
基准测试
- 优化代码,需要对当前代码进行分析
- 内置的测试框架提供了基准测试的能力
基准测试-优化
如rand函数会降低性能
Fastrand()---性能提高
项目实战
- 展示话题和回帖列表
- 暂不考虑前端页面实现,仅实现一个本地web功能
- 话题和回帖数据用文件存储
ER图
实际运用时可参考理清思路
分层结构
- 数据层:数据Model,外部数据的增删改查
- 逻辑层:业务Entity,除了核心业务逻辑输出
- 视图层:视图view,处理和外部的交互逻辑
组件工具
- Gin高性能 go web框架
- Go Mod
go mod init
go get gopkg.in/gin-gonic/gin.v1@v1.3.0
Repository
参照ER图建立话题、回复的结构
- 如何实现查询操作
Repository-index
元数据 索引
数据行 => 内存Map
var(
topicIndexMap map[int64]*Topic
postIndexMap map[int64]*Post
}
//以初始化话题数据索引为例
func initTopicIndexMap(filePath string) error{
open, ere := os.Open(filePath + "topic")
if err != nil{
return err
}
scanner := bufio.NewScanner(open)//基于file初始化scanner,遍历
topicTmpMap := make(map[int64]*Topic)
for scanner.Scan(){
text := scanner.Text()
var topic Topic
if err := json.Unmarshal([]byte(text), &topic); err != nil{
return err
}//转化为结构体储存至内存map
topicIndexMap = topicTmMap
return nil
}
Repository-查询
索引:话题ID
数据:话题
var{
topicDao *TopicDao
topicOnce sync.Once //sync适用于高并发场景下只执行一次的场景
}
func NewTopicDaoInstance() *TopicDao{
topicOnce.Do(
func(){
topicDao = &TopicDao{}
})
return topicDao
}
func(*TopicDao) QueryTopicById(id int64) *Topic{
return topicIndexMap[id]
}
Service
流程:参数校验->准备数据->组装实体
topic与post无先后依赖关系,故可并行处理
func(f *QueryPageInfoFlow) prepareInfo() error{
//获取topic信息
var wg sync.WaitGroup
wg.Add(2)
go func(){...}()
//获取post列表
go func(){...}()
wg.Wait()
return nil
}
Controller
- 构建view对象
- 业务错误码
Router
- 初始化数据索引
- 初始化引擎配置
- 构建路由
- 启动服务
func main(){
if err := Init("./data/"); err != nil{
os.Exit(-1)
}//初始化数据索引
r := gin.Dafault()//初始化引擎配置
r.GET("community/page/get/:id", func(c *gin.Context){
topicId:= c.Param("id")
data := controller.QueryPageInfo(topicId)
c.JSON(200, data)
})//构建路由
err := r.Run()//启动服务
if err != nil{
return
}
}
运行
go run server.go