Go语言进阶与依赖管理 | 青训营笔记

93 阅读4分钟

这是我参与「第五届青训营 」笔记创作活动的第3天

一:Go语言进阶——并发编程

1:并发VS并行

并发:多线程程序在一个核的cpu上运行

image.png

并行:多线程程序在多个核的cpu上运行

image.png

Go可以充分发挥多核优势

2:协程

(1)概念

协程:用户态,轻量级线程,栈MB级别
线程:内核态,线程可以并发跑多个协程,栈KB级别

image.png

(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) //保证子协程在实行完之前主线程不退出
}

输出:可以看出是乱序的,即是并行输出的

image.png

3:CSP

Go提倡通过通信共享内存而不是通过共享内存而实现通信

通过通信共享内存:通道(channel)将几个协程连接,类似传输队列,先进先出,保证收发数据的顺序 ,可以让一个goroutine发出特定的值到另一个goroutine

image.png

通过共享内存实现通信:此做法必须要通过互斥量对内存进行加锁,获取临界区权限,此方法影响程序性能,不同协程之间易矛盾

image.png

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子协程可以保证并发安全

image.png

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)
}

结果:不加锁时结果是不稳定的,可能正确也可能错误;加锁时结果正确

image.png

image.png

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()
}

结果:

image.png

二:依赖管理

1:Go依赖管理演进

GOPATH->GO VENDER->GO MUDULE
演化因素:不同环境、项目依赖的版本,控制依赖库的版本

(1)GOPATH

环境变量$GOPATH,也是一个工作区,有三个重要目录:

bin:项目编译的二进制文件
pkg:项目编译的中间产物,加速编译
src:项目源码

实现依赖的方式:
项目代码直接依赖src下的代码; go get下载最新版本的包到src目录下

缺点:在A和B依赖于某一package的不同版本的场景下,无法实现package的多版本控制

image.png

(2)Go Vender

项目目录下新增vendor文件,所有依赖包副本形式放在$ProjectRoot/vendor;
依赖寻址方式:vendor => GOPATH
通过每个项目引入一份依赖的副本,解决了多个项目需要同一个 package依赖的冲突问题。

缺点:无法控制依赖的版本,而更新项目又可能出现依赖冲突,导致编译错误

image.png

(3)Go Module

作用:定义版本规则和管理项目依赖关系
做法:通过go.mod文件管理依赖包版本;通过go get/go mod指令工具管理依赖包

2:依赖管理三要素

①配置文件,描述依赖:go.mod
②中心仓库管理依赖库:Proxy
③本地工具:go get/go mod

3:依赖配置

(1)go.mod

image.png
其中单元依赖是最重要的部分,由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

image.png

(4)incompatible

主版本2+模块会在模块路径增加/vN后缀
对于没有go.mod文件并且主版本2+的依赖,会加上+incompatible

(5)依赖关系图

image.png

如果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:

image.png

(7)依赖分发:变量GOPROXY

GOPROXY="https://proxy1.cn, https://proxy2.cn ,direct"
GOPROXY就是一个服务站点URL列表,“direct”表示源站,如果前面的站点如果找不到依赖就会回源到第三方代码平台

image.png

(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:需求用例

image.png

需要有topic和postlist两个实体

ER图:实体关系图

image.png

topic和post的关系就是一对多

3:分层结构

image.png

数据层:数据Model,外部数据的增删改查
逻辑层:业务Entity,处理核心业务逻辑输出,对数据层透明
视图层:视图view,处理和外部的交互逻辑

4:组件工具

下载Gin高性能go web框架:github.com/gin-gonic/g…
添加gin依赖:
image.png

5:实体结构体及查询方法

image.png

利用元数据和索引的方式,将数据行映射为一个内存map:
......