1. 语言进阶
并发编程
并发:时间片切换
并行:多核,多线程同时运行
go语言就是为并发而生的
1.1 goroutine 协程
线程:系统昂贵的资源,内核态,比较消耗资源 栈MB级别 可并发跑多个协程 协程:用户态,轻量级线程, 栈 KB级别 创建和调度由go语言本身完成 go语言一次可以创建上万个协程
开启协程
目标:快速打印 hello goroutine
在调用函数的时候在前面加上go关键字,就能为一个函数创建一个协程来运行
time.Sleep(time.Second):保证子协程执行完之前,主线程不退出
package main
import (
"fmt"
//"sync"
"time"
)
func hello(i int) {
println("hello world : " + fmt.Sprint(i))
}
func main() {
//var wg sync.WaitGroup
for i := 0; i < 5; i++ {
//wg.Add(1)
go func(j int) {
//defer wg.Done()
hello(j)
}(i)
}
//wg.Wait()
time.Sleep(time.Second)
}
输出如下:并非按顺序的,说明是并行的输出
1.2 CSP(communicating sequential processes)
协程通信
提倡通过通信共享内存,而不是通过共享内存而实现通信
通过通信共享内存 :
通道Channel :
通道把协程连接,像是一个通信队列,先入先出,保证收发数据的顺序
通过共享内存实现通信:
临界区
1.3 channel 通道
一种引用类型
###通过make创建
make(chan 元素类型,[缓冲大小])
无缓冲通道:make(chan int)
也被称为同步通道
有缓冲通道:make(chan int,,2)
类似菜鸟驿站:货架容量有限,当放满了,会阻塞,直到有人取走一个包裹,才能继续放进去
例子(channel的具体使用)
要求:
A子协程生产数字
B子协程接受A的数字,并对数字进行平方的计算
主协程接受B子协程的结果,输出最后的平方数
func CalSquare() {
//无缓冲
src := make(chan int)
//有缓冲
dest := make(chan int, 3)
//A
go func() {
defer close(src)
for i := 0; i < 10; i++ {
//把数字发送到src
src <- i
}
}()
//B
go func() {
defer close(dest)
//通过src这个channel实现了A协程和B协程的通信
for i := range src {
dest <- i * i
}
}()
//M
for i := range dest {
//也可以是其他复杂操作
//dest选择有缓冲就是考虑到消费者(这里的M)的消费速度可能比较慢,有一些复杂操作,比较耗时。
//选择有缓冲的通道,就不会因为消费者的问题影响生产者的执行效率
println(i)
}
}
最终输出会按照顺序,即并发安全
1.4 并发安全Lock
上锁
1.5 WaitGroup
之前用sleep,但是不能确定sleep的时间
现在用wait group实现并发任务的同步
waitgorup 内部维护了一个计数器,并且公开了有三个方法:
- add 开启协程 +1;
- done 执行结束 -1;
- wait 主协程阻塞直到计数器为0(表明所有并发任务都完成了)
现在对快速打印hello goroutine的例子进行代码优化,用waitgroup代替sleep
import (
"fmt"
"sync"
)
func hello(i int) {
println("hello world : " + fmt.Sprint(i))
}
func ManyGo() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(j int) {
defer wg.Done()
hello(j)
}(i)
}
wg.Wait()
}
2. 依赖管理
依赖,指用各种开发包
对于单体函数,只需要使用原本的库就可以
但在实际开发中,项目复杂,可以用其他依赖包
2.1 go依赖管理演进
2.1.1 GOPATH
环境变量,一个路径,通到go项目的工作区
这个目录下面的三个分支:
- bin:项目编译的二进制文件
- pkg:(package)项目编译的中间产物,加速编译
- src:项目源码,项目代码直接依赖src下的代码
通过go get 下载最新版本的包到src目录下
弊端:无法实现package的多版本控制
本地有2个项目A和B,两个项目依赖同一个package,但是是同一个package的不同版本。
假设项目A依赖于pkg v1,项目B依赖于pkg v2(属于是v1的升级版)
v1实现了func A(), v2实现了func B()。
由于v2没有实现兼容,也就是说,v2里面没有func A(),把它删掉了
由于都是依赖同一个src的源码,所以项目A和项目B不能同时构建成功。
2.1.2 go vendor
在项目目录下增加一个vendor文件夹,存放所有依赖包的副本
项目的依赖会优先从vendor目录下获取,若vendor里面没有,再去gopath找
通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题
对于上面的两个项目,A项目的vendor下是v1版本,B项目的vendor下是v2版本
弊端
项目A依赖了package B和package C,package B依赖了package D的v1版本,package C依赖了package D的v2版本,并且package D 的v1,v2不兼容
无法控制依赖的版本
更新项目有可能出现依赖冲突,导致编译出错
它依赖的是源码,而不能标识版本
2.1.3 Go Module
依赖管理系统
通过go.mod 文件管理依赖包版本
通过go get/go mod指令工具管理依赖包
最终通过go mod定义版本规则,也可以通过工具管理项目依赖关系
2.2 依赖管理三要素
2.3.1. 配置文件,描述依赖
文件描述依赖了哪些包,包如何定位
go.mod 文件
- 模块路径 标识了一个模块,知道从哪里可以找到这个模块
- 原生库的版本号 不同项目需要的版本可能不一样
- 描述单元依赖 module path + 版本号 这样就唯一地定位某一个仓库的某一个版本
2.3.2 依赖配置-version
go path 和 go vendor 没有版本 go module才有版本定义
语义化版本
${MAJOR}.${MINOR}.${PATCH}
如V1.3.0 ,V2.3.0
MAJOR:大版本,不同的major版本可以不兼容(代码隔离)
MINOR:新增函数/功能,要在同一个major下前后兼容
PATCH:代码bug的修复
基于commit 伪版本
vX.0.0-yyyymmddhhmmss-abcdefgh1234
- 版本前缀,同语义化版本
- 时间戳。提交某次commit的时间戳
- 提交某次commit的哈希码前缀
2.3.3 依赖配置-indirect 关键字
A依赖了B,B依赖了C,记作 A->B->C
A->B:直接依赖
A->C:间接依赖
在go.mod中,间接依赖会用indirect标识出来
2.3.4 依赖配置-incompatible(不兼容)
主版本(MAJOR)2+的模块要在模块路径增加/vN的后缀,如example/lib5/v3 v3.0.2
对于没有go.mod文件并且主版本2+的依赖,要在版本号后面+incompatible 。如example/lib6 v3.2.0+incompatible ,标识出来可能存在不兼容的代码逻辑
2.3.4 依赖配置-依赖图
依赖图如下:
项目X依赖了A、B两个项目,且A、B分别依赖了C项目的v1.3 和 v1.4版本,最终编译时所使用的C项目的版本为v1.4(选择最低的兼容版本)
即使C有1.5,也是之选1.4版本
2.3.5 依赖分发
回源
依赖去哪里下载,如何下载 直接从代码托管平台(如GitHub)下载对应版本的依赖,存在以下问题:
- 无法保证构建稳定性
软件的作者可以直接在代码托管平台 增加/修改/删除软件版本,下次去构建项目的时候,之前依赖的版本可能找不到了 - 无法保证依赖可用性
作者可以对它的仓库 删除软件 - 增加第三方压力
代码托管平台负载问题
proxy
一个服务站点
会缓存源站中的软件内容,缓存的软件版本不会改变
实现了稳定、可靠的依赖分发
使用了go proxy,以后就直接从proxy直接拉取依赖
没有问题是proxy解决不了的,解决不了就两层proxy
2. 中心仓库管理依赖库
gomodule里面的Proxy
2.3.6 依赖分发-变量 GOPROXY
GOPROXY是环境变量,url列表,用逗号分割
例:GOPROXY = "https://proxy1.cn,https://proxy2.cn,direct"
先在proxy1 中找依赖,proxy1中没有就去proxy2
最后的direct表示源站:若前面的站点都没有依赖就回源到第三方代码平台上去
2.3.7 工具-go get
直接go get example.org/pkg,默认拉取MAJOR的最新版本的提交
若加
@update 默认,同上
@none 删除依赖
@v1.1.2 tag版本,语义版本
@23dfdd5 特定的commit
@master 指定分支,拉取分支的最新commit
3. 本地工具
go get 和 go mod
2.3.8 工具-go mod
go mod init 初始化,创建go.mod文件(在初始化项目的时候用)
go mod download 下载模块到本地缓存(把所有的依赖拉下来)
go mod tidy 增加需要的依赖,删除不需要的依赖(在go.mod文件里,之前用过某些依赖,但经过代码的修改,现在已经不需要了,通过tidy 就可以把这些非必要的依赖删除,减少构建整个项目的时间
3. 测试
事故
- 营销配置错误,导致非预期用户享受权益
- 代码逻辑错误,广告位被占
- 代码指针使用错误,导致app无法使用
分类
- 回归测试:
质量保证,手动通过终端回归一些固定的主流场景。如:刷抖音,看评论等。 - 集成测试:
对系统功能的一种测试验证。 通过服务暴露的某个接口,进行一些自动化的回归测试
集成一个功能 - 单元测试:
测试开发阶段,开发者对单独的函数、模块做功能验证 从上至下,覆盖率逐层变大,成本却逐层降低 单元测试的覆盖率在一定程度上决定了代码的质量
3.1 单元测试
输入->测试单元->输出(和期望输出校对,验证代码的正确性)
这里的测试单元可能是函数、模块、一些聚合的大的函数等等
可以保证质量,提高效率
3.1.1 单元测试-规则
- 所有文件以_test.go结尾
通过展开目录,可以分出哪些是源代码,哪些是测试代码 - func TestXxx(*testing.T)
测试函数命名规范 Test开始,Xxx指的是大写首字母 如func TestPublishPost(t *testing.T) - 初始化逻辑放到TestMain中
func TestMaiin(m *testing.M){
//测试前:数据装载、配置初始化等前置工作
code := m.Run()//跑这个package下的所有单元测
//测试后:释放资源等收尾工作
os.Exit(code)
}
3.1.5 单元测试-覆盖率
go test 文件名 --cover
cover参数,计算覆盖率
对各分支分别测试
一般覆盖率:50%~60% 主流程
较高覆盖率:80%(资金类的操作,要求比较高)
测试分支相互独立、全面覆盖(不重不漏)
测试单元粒度足够小,函数单一职责
3.2 单元测试-依赖
单元可能依赖某个本地文件,数据库DB,或者是cache
幂等:每次测试的结果一样
稳定:测试能够相互隔离,单元测试能在任何时间,任何函数独立运行
若直接测试,调到DB或者是cache,是不稳定的,因为依赖网络。所以实际的单元测试中会用到mock机制
3.3 单元测试-文件处理
func ReadFirstLine() string{
open, err := os.Open("log")
defer open.Close()
if err ≠ nil{
return ""
}
scanner := bufio.NewScanner(open)
for scanner.Scan(){
return scanner.Text()
}
return ""
}
func ProcessFirstLine() string{
line := ReadFirstLine()
destLine := strings.ReplaceAll(line, "11", "00")
return destLine
}
func TestProcessFirstLine (t *testing.T){
line := ReadFirstLine()
assert.Equal(t, "line00", firstLine)
}
以上代码是依赖于“log”文件的 若“log”文件修改或删除了,测试就无法运行了
3.4 单元测试-mock
快速mock函数:为一个函数/方法打桩
用函数A替换函数B,B是原函数,A是打桩函数
func Patch(target, replacement interface{}) *PatchGuard{
//target是原函数,要被替换的函数
//replacement是打桩函数
//将内存中函数的地址替换成运行时函数的地址,这样在真正测试的时候运行的就是打桩函数,就实现了mock的功能
t := reflect.ValueOf(target)
r := reflect.ValueOf(replacement)
patchValue(t,r)
return &PatchGuard{t,r}
}
func Unpatch(target interface{}) bool{
//测试结束后,把桩卸载
return unpatchValue(reflect.ValueOf(target))
}
对于上面的那个firstline的例子,现在用上mock
对ReadFirstLine打桩测试,不再依赖本地文件
func TestProcessFirstLineWithMock(t *testing.T){
monkey.Patch(ReadFirstLine, func()string{
return "line110"
})
defer monkey.Unpatch(ReadFirstLine)
line := ProcessFirstLIne()
assert.Equal(t, "line000", line)
}
3.5 基准测试
测试一段程序运行时的性能和CPU损耗
3.5.1 例子-服务器负载均衡
有10个服务器,随机选择一个服务器执行
import(
"math/rand"
)
var ServerIndex [10]int
func InitServerIndex(){
for i := 0; i<10; i++ {
ServerIndex[i] = i+100
}
}
func Select() int{
return ServerIndex[rand.Intn(10)]
}
对select函数做基准测试
规则和单元测试类似,只不过它用Benchmark开头
func BenchmarkSelect(b *testing.B){
InitServerIndex()
//定时器重置。上面那个函数不属于我们要测试的范围,所以不应该算上它的耗时,定时器重置。
b.ResetTimer()
//做串行的压力测试
for i:=0; i<b.N; i++{
Select()
}
}
//并行的测试
func BenchmarklSelectParallel(b *testing.B){
InitServerIndex()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB){
for pb.Next(){
Select()
}
})
}
用bench参数执行基准测试go test -bench,会显示CPU耗时。
结果显示,这个select函数并行情况下耗时会更多,性能劣化。主要原因是这个select用到了rand函数。
rand函数为了保证全局的随机性和并发安全,持有一把全局锁,这样就降低了并发性能
3.5.3 基准测试-优化
为了解决随机函数rand的性能问题,开源了一个fastrand函数
用fastrand比用rand的并行提高了百倍
fastrand牺牲了随机数列的一致性
4. 项目实战
4.1需求描述
社区话题页面
- 展示话题(标题,文字描述)和回帖列表
- 暂不考虑前端页面实现,仅仅实现一个本地web服务
- 话题和回帖数据用文件存储,不用数据库
4.2 需求用例
浏览消费用户:浏览页面,包含topic(话题) 和 postlist(帖子)
4.3 ER图 entity relationship diagram
ER图表征了现实世界的概念模型
有两个实体:topic 和 post
topic 和 post 的关系:一对多
4.4 分层结构
三层:
数据层repository:关联底层的数据模型model,分装外部数据的增删改查。根据我们的需求,数据会存储在本地文件上,通过本地的操作拉取话题和帖子的数据。
数据层主要面向service层,对service层透明。
repository会屏蔽下游的数据差异,service层不需要关心数据的存储方式,只要拿到repository层返回的model数据就行
逻辑层service 业务
处理核心业务逻辑输出
接收repository层的数据做打包封装,输出一个实体entity
对应我们的需求,这个entity就是话题页面,并上送给视图蹭饭
视图层controller
处理和外部的交互逻辑,以view视图的形式返回给客户端
对上游负责,包装一些数据格式
我们的需求,会json格式化结果,封装,以api的形式进行访问
不同项目有不同层次的拆分,根据项目的进行具体拆分,无需套用
4.5 组件工具
- Gin高性能 go web 框架
- go mod
go mod init初始化go.mod文件
4.6 repository
topic和post都是json数据 两个结构体
基本查询操作
实现两个基本的查询操作:
- QueryTopicById:通过话题id查询话题
- QueryPostsByParentId:通过话题id查询到和话题关联的所有帖子
index
可以在文件里面,全扫描遍历的方式查询,一个一个地对比,但不够高效
这里用到索引 map[id] = &topic
查询
地址:直接用map就是了
sync.Once: 适合高并发场景下只执行一次的时候(单例模式)
4.7 service
参数校验->准备数据(从repository层拿到的数据)->组装实体
准备数据:
获取repository层里的话题数据和帖子数据
由于这两个都是依赖的tipic id,两个之间没有相互依赖,所以可以采取并行方案,提高效率
并行用到的waitgroup 之前有讲到
4.8 controller
pagedata 的data 就用 pageinfo赋值
4.9 router
gin 搭建外部框架
初始化数据索引
生成内存map索引
初始化引擎配置
gin.Default()
构建路由
还不懂