Goroutine
协程:用户态,轻量级线程,栈 KB 级别
线程:内核态,线程跑多个协程,栈 MB 级别
func hello(i int) {
println("hello goroutine " + fmt.Sprint(i))
}
func HelloGoGoRoutine() {
for i := 0; i < 5; i++ {
go func(j int) {
hello(j)
}(i) //调用匿名函数,把i作为参数传入
}
time.Sleep(time.Second) // 确保协程执行完之前,主协程没有退出}
}
CSP (communicating sequential processes)
提倡通过通信共享内存而不是通过共享内存实现通信
Channel
make(chan 元素类型,[缓冲大小])
- 无缓冲通道: make(chan int)
- 有缓冲通道: make(chan int, 2)
并发安全lock
用sync相关关键字,实现并发安全操作和协程间的同步。
WaitGroup
计数器在开启协程的时候加一,执行结束减一,主协程阻塞直到计数器为0
依赖管理
依赖管理演进
GOPATH ---> Go Vendor ---> Go Module
- 间接依赖最终编译时使用兼容的最低版本
GOPATH
GOPATH是Go语言支持的一个环境变量,value是Go项目的工作区。
目录有以下结构:
- src:存放Go项目的源码
- pkg:存放编译的中间产物,加快编译速度
- bin:存放Go项目编译生成的二进制文件
项目代码直接依赖src下的代码, go get 下载最新版本的包到 src 目录下
弊端:无法实现package的多版本控制
Go Vendor
- 项目目录下增加 vendor文件,所有依赖以副本形式放入
- 解决了多个项目用同一个package依赖冲突问题
- 依赖寻址方式: vendor ---> GOPATH
弊端:无法控制依赖的版本,项目A可能间接依赖同一个package的不同版本,更新容易出现依赖冲突
Go Module
- 通过 go.mod文件管理依赖包版本
- 通过 go get /go mod 指令工具管理依赖包
依赖管理三要素
- 配置文件,描述依赖 go.mod
- 中央仓库管理依赖库 proxy
- 本地工具 go get/mod
依赖配置 go.mod
依赖标识: [Module Path][version]
- 模块路径用来标识一个模块,从模块路径可以看出从哪里找到该模块
- 如果项目的子包想被单独引用,则需要通过单独的init go.mod文件进行管理。
- 再下面是依赖的原生sdk版本
- 最下面是单元依赖,
- 每个依赖单元用模块路径+版本来唯一标示。
依赖配置-version
gopath和govendor都是源码副本方式依赖,没有版本规则概念,而gomod为了放方便管理则定义了版本规则, 分为语义化版本和基于commit的伪版本。
其中语义化版本包括:[Minor].$[PATCH]
- 不同的MAJOR 版本表示是不兼容的 API,所以即使是同一个库,MAJOR 版本不同也会被认为是不同的模块;
- MINOR 版本通常是新增函数或功能,向后兼容;
- 而patch 版本一般是修复 bug ;
而基于commit的为版本包括:vX.0.0-yyyymmddhhmmss-abcdefabcdef1234
- 基础版本前缀是和语义化版本一样的;
- 时间戳 (yyyymmddhhmmss), 也就是提交 Commit 的时间
- 最后是校验码 (abcdefabcdef1234), 包含 12 位的哈希前缀;
- 每次提交commit后 Go 都会默认生成一个伪版本号。
依赖配置 indirect后缀
标示非直接依赖
依赖配置 incompatible
- 主版本2+模块会在模块路径增加/vN后缀,这能让go module按照不同的模块来处理同一个项目不同主版本的依赖。例子:由于gomodule是1。11实验性引入所以这项规则提出之前已经有一些仓库打上了2或者更高版本的tag了,为了兼容这部分仓库,对于没有go.mod文件并且主版本在2或者以上的依赖,会在版本号后加上+incompatible 后缀
- 前面讲语义化版本提到,对于同一个库的不同的major版本,需要建立不同的pkg目录,用不同的go mod文件管理,如下面仓库为例,V1版本go mod在主目录下,而对于V2版本,则单独建立了V2目录,用另一个go mod文件管理依赖路径,来表明不同major的不兼容性。,那对于有些V2+tag版本的依赖包并未遵循这一定义规则,就会打上+incompatible 后缀,增加一个compatile的case
工具 -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 增加需要的依赖,删除不需要的依赖
测试
以下从上到下覆盖层逐层变大,成本逐渐降低
- 回归测试
- 集成测试
- 单元测试
单元测试
单元测试主要包括,输入,测试单元,输出,以及校对,单元的概念比较广,包括接口,函数,模块等;用最后的校对来保证代码的功能与我们的预期相符;
一方面可以保证质量,在整体覆盖率足够的情况下,一定程度上既保证了新功能本身的正确性,又未破坏原有代码的正确性。另一方面可以提升效率,在代码有bug的情况下,通过编写单测,可以在一个较短周期内定位和修复问题
单元测试规则
- 所有测试文件以_test.go结尾
- func TestXxx(*testing.T)
- 初始化逻辑放到 TestMain 中
运行
go test [flags][packages]
覆盖率测试:go test [test.go] [tobetest.go] --cover
- 一般覆盖率:50%-60%,较高覆盖率80%+
- 测试分支相互独立、全面覆盖
- 单元测试粒度够小,函数单一职责
mock
而我们的单测需要保证稳定性和幂等性,稳定是指相互隔离,能在任何时间,任何环境,运行测试。 幂等是指每一次测试运行都应该产生与之前一样的结果。而要实现这一目的就要用到mock机制。
快速mock 函数
- 为一个函数打桩
- 为一个方法打桩
- 这里我们用了Monkey,monkey是一个开源的mock测试库,可以对method,或者实例的方法进行mock,反射,指针赋值Mockey Patch 的作用域在 Runtime,在运行时通过通过 Go 的 unsafe 包,能够将内存中函数的地址替换为运行时函数的地址。,将待打桩函数或方法的实现跳转到。
感觉像是直接跳过部分函数固定其返回好测试后面的是可以说的吗,不过很适合调试代码
基准测试
Go 语言还提供了基准测试框架,基准测试是指测试一段程序的运行性能及耗费 CPU 的程度。而我们在实际项目开发中,经常会遇到代码性能瓶颈,为了定位问题经常要对代码做性能分析,这就用到了基准测试。使用方法类似于单元测试,能够
- 优化代码,需要对当前代码分析
- 内置的测试框架提供了基准测试的能力
运行和优化
讲的东西还不是很懂,先不记哩
项目实践
分层结构
File ---> repository ---> service ---> controller ---> client
model entity view
数据层 逻辑层 视图层
- 数据层:数据model,外部数据的增删改查数据层面向逻辑层,对service层透明,屏蔽下游数据差异,也就是不管下游是文件,还是数据库,还是微服务等,对service层的接口模型是不变的。
- 逻辑层:业务entity,处理核心业务逻辑输出。计算打包业务实体entiy,对应我们的需求,上送给视图层
- 视图层:视图view,处理和外部的交互逻辑,对于我们需求,我们封装json格式化的请求结果,api形式访问
组件工具
- Gin 高性能 go web 框架
- Go mod
实战
数据层
一方面查询我们可以用全扫描遍历的方式,但是这虽然能达到我们的目的,但是并非高效的方式,所以这里引出索引的概念,索引就像书的目录,可以引导我们快速查找定位我们需要的结果;这里我们用map实现内存索引,在服务对外暴露前,利用文件元数据初始化全局内存索引,这样就可以实现0(1)的时间复杂度查找操作。
分号主要是一行内语句的分割
直接根据查询key获得map中的value就好了,这里用到了sync.once,主要适用高并发的场景下只执行一次的场景,这里的基于once的实现模式就是我们平常说的单例模式,减少存储的浪费。 有了topic的查询代码,大家可以照猫画虎 自行实现一下根据话题id查询回帖列表的查询方法
service层/逻辑层
参数校验---> 准备数据---> 组装实体
感觉这里像pojo里面非data层()
但是又有service的功能
在后期做项目开发中,一定要思考流程是否可以并,通过压榨CPU,降低接口耗时,不要一味的串行实现,浪费多核cpu的资源
controller层
返回的错误码在这里写结构体
router层
- 初始化数据索引
- 初始化引擎配置
- 构建路由
- 启动服务
是web服务的引擎配置,包括 ***** ,path映射到具体的controller。通过path变量传递话题id