这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记
思维导图
图片格式
文本格式
-
并发编程\
-
协程 Goroutine\
-
线程与协程
\
- 协程:用户态,轻量级线程,栈MB级别\
- 线程:内核态,线程跑多个协程,栈KB级别\
- 多个协程之间的执行并没有按顺序
\
-
-
通道 Channel\
-
提倡通过通信共享内存,而不是通过共享内存实现通信
\
- channel是Go中代替共享内存的通信方式,channel从底层实现上是一种队列\
- 在使用的时候需要通道的发送方和接收方需要知道数据类型和具体通道\
- 如果有一端没有准备好或消息没有被处理会阻塞当前端。\
-
通道构建
\
-
make(chan 元素类型,[缓冲大小])\
- 无缓冲通道 make(chan int)\
- 有缓冲通道 make(chan int,2)\
-
-
示例
\
- 通过通信实现共享内存\
- 输出是按顺序的-->channel是并发安全的
channel 本身是由 go 原生保证并发安全的,不用额外的同步措施,可以放心使用。
-
解决golang编译提示dial tcp 172.217.160.113:443: connectex: A connection attempt failed\
- 命令行执行go env -w GOPROXY=goproxy.cn\
-
-
锁 Lock
pkg.go.dev/sync- go也保留了共享内存的通信\
-
共享内存不加锁出现的并发安全问题
\
- 不加锁不能得到预期结果
\
- 不加锁不能得到预期结果
-
线程同步 WaitGroup
pkg.go.dev/sync- 子协程的执行时间未知,通过WaitGroup实现协程同步,取代暴力Sleep\
-
WaitGroup暴露的三个方法
\
-
WaitGroup维护一个计数器\
- 开启协程+1;\
- 执行结束-1;\
-
主协程阻塞直到计数器为0。\
- 计数器为0就代表所有并发任务执行完成\
-
示例
\
- defer等待该部分执行完毕-->等待这个线程执行完毕\
-
-
-
依赖管理
\
-
GOPATH\
- go语言一个环境变量,是go项目工作区\
-
目录构成
\
- bin-项目编译的二进制文件\
- pkg-项目编译的中间产物,加速编译\
- src-项目源码\
-
实现逻辑\
- go get下载最新版本的包到src目录下\
- 项目代码所有依赖,直接依赖src下的代码\
-
弊端\
-
场景:A和B依赖于某一package的不同版本
\
- 问题:无法实现package的多版本控制\
- src下只能有一个版本存在,如果多个项目依赖同一个库,则依赖该库是同一份代码,所以不同项目不能依赖同一个库的不同版本\
-
-
Go Vendor
\
- Vendor是当前项目中的一个目录,其中存放了当前项目依赖的副本。\
-
实现逻辑\
- 在Vendor机制下,如果当前项目存在Vendor目录,会优先使用该目录下的依赖\
- 如果依赖不存在,会从GOHATH中寻找\
-
弊端\
-
vendor无法很好解决
\
- 依赖包的版本变动问题\
-
一个项目依赖同一个包的不同版本的问题\
- 更新项目又可能出现依赖冲突,导致编译出错。\
- 根本原因:依赖的是源码,而无法解决版本问题\
-
-
Go Module
go.dev/blog/using-…- 终极目标:定义版本规则和管理项目依赖关系\
-
实现逻辑\
- 通过go.mod文件管理依赖包版本\
-
通过go get/go mod 指令工具管理依赖包\
- 定义规则\
- 管理版本依赖关系\
-
依赖管理三要素\
-
1、配置文件,描述依赖\
- go.mod\
-
2、中心仓库管理依赖库\
- Proxy\
-
3、本地工具\
- go get/mod\
-
-
依赖配置\
- go.mod
\
-
version(版本号)\
- gopath和govendor都是源码副本方式依赖,没有版本规则,而gomod为了放方便管理则定义了版本规则\
-
分为
\
-
不同的MAJOR版本表示是不兼容的APl(代码隔离)\
- 所以即使是同一个库,MAJOR版本不同也会被认为是不同的模块\
- MINOR版本通常是新增函数或功能,向后兼容\
- PATCH版本一般是修复bug\
-
基于comit的伪版本包括基础版本前缀是和语义化版本一样的\
- 时间戳(..ymmddhmss,也就是提交Commit的时间\
- 最后是校验码(abcoefbcef)包含12位的哈希前缀\
- 每次提交commit后Go都会默认生成一个伪版本号\
-
-
indirect
\
- 直接依赖与间接依赖
\
- 直接依赖与间接依赖
-
incomptile
\
-
go要求主版本(MAJOR)2+的模块应在模块路径增加/vN后缀
\
-
这能让go module按照不同的模块来处理同一个项目不同主版本的依赖\
- 同一依赖不同MAJOR版本之间的代码隔离\
-
- 但是由于gomodule是1.11引入,所以这项规则提出之前已经有一些仓库打上了2或者更高版本的tag了,为了兼容这部分仓库,对于没有go.mod文件并且主版本在2或者以上的依赖,会在版本号后加上+incompatible后缀\
-
-
依赖图
\
- C1.3 C1.4都是属于1系列版本的,前后兼容,go选择能让A B都满足的最低兼容版本,即C1.4\
- 因为C1.3无法满足B的要求\
- 此时,即使有C1.5 go也不会选择\
- go.mod
-
依赖分发\
- 直接从管理仓库拉取依赖存在的问题
\
-
Proxy(解决方案)
\
- Go Proxy是一个服务站点,它会缓源站中的软件内容,缓存的软件版本不会改变,并且在源站软件删除之后依然可用\
- 使用Go Proxy 之后,构建时会直接从Go Proxy站点拉取依赖\
-
使用
\
- GoModules通过GOPROXY环境变量控制如何使用Go Proxy\
-
GOPROXY是一个Go Proxy 站点URL列表\
- 用逗号分割\
- 可以使用"direct"表示源站(代理中都不存在,就从代码托管平台拉取)\
- 对于示例配置,整体的依赖寻址路径,会优先从proxy1下载依赖,如果proxy1不存在,后下钻proxy2寻找,如果proxy2,中不存在则会回源到源站直接下载依赖,缓存到proxy站点中\
- 直接从管理仓库拉取依赖存在的问题
-
工具\
- go get
\
- go mod
\
- go get
-
-
单元测试\
-
单元测试概念和规则
go.dev/doc/tutoria…- 单元测试主要包括,输入,测试单元,输出,以及校对,单元的概念比较广,包括接口,函数,模块等\
-
基本规范\
- 所有测试文件以_test.go结尾
\
-
测试函数命名规范\
- func TestXxx(t *testing.T)
\
- func TestXxx(t *testing.T)
-
初始化逻辑放到TestMain 中
\
- m.Run() 代表跑包下所有单测\
- 所有测试文件以_test.go结尾
-
例子
\
- 运行
\
- 运行
- assert\
-
覆盖率\
- 在实际项目中,一般的要求是50%~60%覆盖率,而对于资金型服务,覆盖率可能要求达到80%\
-
示例
\
-
执行命令,查看测试覆盖率
\
- go test 测试.go 被测试.go --cover
\
- go test 测试.go 被测试.go --cover
-
-
单元测试要求\
- 测试分支相互独立、全面覆盖。\
- 测试单元粒度足够小,实现代码函数体足够小,方便单元测试\
- 这样就比较简单的提升覆盖率,也符合函数设计的单一职责\
-
依赖
\
-
幂等\
- 重复运行case结果一样\
-
稳定\
-
单元测试相互隔离,每个单元测试可以任何时间独立运行\
- Mock\
-
文件处理
\
- 将文件中的第一行字符串中的11替换成00,执行单测,测试通过,而我们的单测需要依赖本地的文件,如果文件被修改或者删除测试就会失败。为了保证测试的确稳定性,我们对读取文件函数进行mock,屏蔽对于文件的依赖\
-
-
-
Mock 测试
github.com/bouk/monkey-
原理\
- 在运行时通过通过Go的unsafe包,能够将内存中函数的地址替换为运行时函数的地址,实现最终测试时运行打桩函数\
- Patch
\
- Unpatch
\
-
-
-
作用\
- 基准测试是指测试一段程序的运行性能及耗费CPU的程度\
- 在实际项目开发中,经常会遇到代码性能瓶颈,为了定位问题经常要对代码做性能分析\
-
示例
\
- 基准测试应该以BenchmarkXxxx命名,参数类型为testing.B\
- 真正计时内容开始前,进行计时器重置\
- fastrand.Intn()性能优化
\
-
-
-
项目实战\
-
需求模型来源\
- 青训营话题页
forum.juejin.cn/youthcamp/p…
- 青训营话题页
-
需求\
- 实现一个展示话题(标题,文字描述)和回帖列表的后端 http 接口;\
- 本地文件存储数据\
- 需求用例
\
- ER图
\
-
组件及技术点\
-
分层结构设计
github.com/bxcodec/go-…- 整体分为三层,repository数据层,service逻辑层,controoler视图层,\
-
数据层面向逻辑层,对service层透明,屏蔽下游数据差异,也就是不管下游是文件,还是教据库套基微服务等,对service层的接口模型是不变的。\
- 输出model\
-
servcie逻辑层处理核心业务逻辑,打包封装数据层\
-
输出entiy\
- 对应项目中话题页面,包括话题和回帖列表,并上送给视图层;\
-
-
Controller视图层负责处理和外部的交互逻辑\
- 输出以view视图的形式返回给客户端\
- 对于我们需求,我们封装json格式化的请求结果,api形式访问\
- web 框架:Gin
-github.com/gin-gonic/g…
-
Repository\
- 话题和回帖实际上就是两个JSON
\
-
Index
\
- 定义topic 和post索引
\
- 将数据行映射为内存的map
\
- 定义topic 和post索引
- 查询
\
- 文件操作:读文件
pkg.go.dev/io
- 数据查询:索引
www.baike.com/wikiid/5527…
- 话题和回帖实际上就是两个JSON
-
Service\
- 实体
\
-
流程
\
-
参数校验\
- 对传入的topic ID做非法校验\
-
准备数据\
- 通过Repository拿数据\
- 组装实体\
-
对应上述流程的流程编排代码
\
- prpareInfo
\
- prpareInfo
-
- 实体
-
Controller\
- 这里我们定义一个view对象
\
- 通过code msg打包业务状态信息,用data承载业务实体信息
\
-
Router
\
- 初始化数据索引\
- 初始化引擎配置\
- 构建路由\
- 启动服务\
- 这里我们定义一个view对象
-
-
启动\
- 启动后,PowerShell里通过crul命令请求curl 127.0.0.1:8080/community/page/get/1\
-