这是我参与「第五届青训营 」笔记创作活动的第3天
一:Go语言进阶——并发编程
1:并发VS并行
并发:多线程程序在一个核的cpu上运行
并行:多线程程序在多个核的cpu上运行
Go可以充分发挥多核优势
2:协程
(1)概念
协程:用户态,轻量级线程,栈MB级别
线程:内核态,线程可以并发跑多个协程,栈KB级别
(2)如何使用协程
只需要在函数前加go即可
func hello(i int) {
println("hello goroutine:" + fmt.Sprint(i))
}
func HelloGoRoutine() {
for i := 0; i < 5; i++ {
go func(j int) { //函数前加go
hello(j)
}(i)
}
time.Sleep(time.Second) //保证子协程在实行完之前主线程不退出
}
输出:可以看出是乱序的,即是并行输出的
3:CSP
Go提倡通过通信共享内存而不是通过共享内存而实现通信
通过通信共享内存:通道(channel)将几个协程连接,类似传输队列,先进先出,保证收发数据的顺序 ,可以让一个goroutine发出特定的值到另一个goroutine
通过共享内存实现通信:此做法必须要通过互斥量对内存进行加锁,获取临界区权限,此方法影响程序性能,不同协程之间易矛盾
4:channel
引用类型,需要通过make创建
无缓冲通道make(chan int),也称为同步通道,发送方和接收方同步
有缓冲通道make(chan int,2),缓冲区满了就不能发送了,可以解决生产协程和消费协程速度不匹配的问题
例子:A子协程负责生产数字,B子协程负责计算平方,主协程最后输出结果
func CalSquare() {
//创建两个子协程
src := make(chan int)
//B子协程作为消费方,会比A子协程生产数字较慢,所以用缓冲区来保证B不影响A的生产
dest := make(chan int, 3)
//A子协程负责生产数字
go func() {
defer close(src)
for i := 0; i < 10; i++ {
src <- i
}
}()
//B子协程通过遍历A子协程实现计算平方
go func() {
defer close(dest)
for i := range src {
dest <- i * i
}
}()
//主协程负责遍历输出B子协程
for i := range dest {
println(i)
}
}
输出结果可以保证顺序,即AB子协程可以保证并发安全
5:并发安全 Lock使用
Go保留了通过共享内存实现通信的操作,存在同时操作一个内存的情况,此时需要使用Lock锁
例子:5个协程并发执行,对一个变量进行2000次加一的操作,预期结果为5*2000=10000
定义变量:
var (
x int64
lock sync.Mutex
)
分别定义加锁和不加锁的两个方法:
func addWithLock() {
for i := 0; i < 2000; i++ {
//每次+1之前都加锁,临界区保护
lock.Lock()
x += 1
//+1结束,释放锁
lock.Unlock()
}
}
func addWithoutLock() {
for i := 0; i < 2000; i++ {
x += 1
}
}
主协程分别调用两个方法:
func Add() {
//不加锁
x = 0
for i := 0; i < 5; i++ {
go addWithoutLock()
}
time.Sleep(time.Second)
println("without lock:", x)
//加锁
x = 0
for i := 0; i < 5; i++ {
go addWithLock()
}
time.Sleep(time.Second)
println("with lock:", x)
}
结果:不加锁时结果是不稳定的,可能正确也可能错误;加锁时结果正确
6:WaitGroup
作用:实现并发任务的同步
过程:开启协程+1,执行结束-1,主协程阻塞直到计数器为0
三个方法:add(n int),计数器+n;done(),计数器-1;wait(),阻塞直到计数器为0
例子:启动n个并发任务,则计数器+n,每个任务完成时计数器-1,最后调用wait阻塞等待所有任务完成
例子:
func hello(i int) {
println("hello goroutine:" + fmt.Sprint(i))
}
func ManyGoWait() {
var wg sync.WaitGroup
//开启5个协程
wg.Add(5)
for i := 0; i < 5; i++ {
go func(j int) {
//该协程执行结束,-1
defer wg.Done()
hello(j)
}(i)
}
//阻塞
wg.Wait()
}
结果:
二:依赖管理
1:Go依赖管理演进
GOPATH->GO VENDER->GO MUDULE
演化因素:不同环境、项目依赖的版本,控制依赖库的版本
(1)GOPATH
环境变量$GOPATH,也是一个工作区,有三个重要目录:
bin:项目编译的二进制文件
pkg:项目编译的中间产物,加速编译
src:项目源码
实现依赖的方式:
项目代码直接依赖src下的代码;
go get下载最新版本的包到src目录下
缺点:在A和B依赖于某一package的不同版本的场景下,无法实现package的多版本控制
(2)Go Vender
项目目录下新增vendor文件,所有依赖包副本形式放在$ProjectRoot/vendor;
依赖寻址方式:vendor => GOPATH
通过每个项目引入一份依赖的副本,解决了多个项目需要同一个 package依赖的冲突问题。
缺点:无法控制依赖的版本,而更新项目又可能出现依赖冲突,导致编译错误
(3)Go Module
作用:定义版本规则和管理项目依赖关系
做法:通过go.mod文件管理依赖包版本;通过go get/go mod指令工具管理依赖包
2:依赖管理三要素
①配置文件,描述依赖:go.mod
②中心仓库管理依赖库:Proxy
③本地工具:go get/go mod
3:依赖配置
(1)go.mod
其中单元依赖是最重要的部分,由modulepath+版本号组成,作为依赖的标识
(2)关于version定义
①语义化版本:由三个部分组成
${MAJOR}.${MINOR}.${PATCH}
其中,major是大版本,之间可以不兼容;minor是新增功能等,需要在当前major下保证兼容;patch就是代码修复
例子:V1.3.0;V2.3.0
②基于commit的伪版本:由前缀码-commit时间戳-12位哈希码前缀
vX.0.0-yyyymmddhhmmss-abcdefgh1234
例子:v0.0.0-20220401081311-c38fb59326b7
(3)直接依赖与间接依赖
A依赖B,B依赖C,则A与B是直接依赖,A与C是间接依赖,间接依赖要标识indirect
(4)incompatible
主版本2+模块会在模块路径增加/vN后缀
对于没有go.mod文件并且主版本2+的依赖,会加上+incompatible
(5)依赖关系图
如果X项目依赖了A、B两个项目,且A、B分别依赖了C项目的v1.3、v1.4两个版本,最终编译时所使用的C项目的版本为如下哪个选项?(单选)
A.v1.3
B.v1.4
C.A用到C时用v1.3编译,B用到C时用v1.4编译
答案是B,选择最新的兼容版本,1.3和1.4之间肯定兼容
(6)依赖分发:回源
直接使用版本依赖仓库下载依赖的弊端:
①无法保证构建稳定性:平台可以直接增加/修改/删除软件版本
②无法保证依赖可用性:作者可以删除软件
③增加第三方压力:代码托管平台负载问题
Proxy:
(7)依赖分发:变量GOPROXY
GOPROXY="https://proxy1.cn, https://proxy2.cn ,direct"
GOPROXY就是一个服务站点URL列表,“direct”表示源站,如果前面的站点如果找不到依赖就会回源到第三方代码平台
(8)工具:go get
直接使用go get会默认找major版本的最新的提交,其不同后缀有不同的功能
①@update:默认
②@none:删除依赖
③@v1.1.2:tag版本,拉取特定语义版本
④@23dfdd5:拉取特定的commit版本
⑤@master:拉取分支的最新commit
(9)工具:go mod
①init:初始化,创建go.mod文件
②download:下载模块到本地缓存
③tidy:增加需要的依赖,删除不需要的依赖
三:测试
四:项目实践
1:需求描述
社区话题页面:
①展示话题(标题,文字描述)和回帖列表
②暂不考虑前端页面实现,仅仅实现一个本地web服务
③话题和回帖数据用文件存储
2:需求用例
需要有topic和postlist两个实体
ER图:实体关系图
topic和post的关系就是一对多
3:分层结构
数据层:数据Model,外部数据的增删改查
逻辑层:业务Entity,处理核心业务逻辑输出,对数据层透明
视图层:视图view,处理和外部的交互逻辑
4:组件工具
下载Gin高性能go web框架:github.com/gin-gonic/g…
添加gin依赖:
5:实体结构体及查询方法
利用元数据和索引的方式,将数据行映射为一个内存map:
......