Go 语言进阶与依赖管理
这一节课讲了一些进阶的知识,例如并发,有 Goroutine 和Channel,h合理运用这些特性能让我们的程序运行的更快和安全。
1. 并发编程:
1.1 Goroutine:
-
协程:用户态,轻量级线程,栈KB级别。
-
线程:内核态,线程跑多个协程,栈MB级别。
func hello(i int) {
println("hello world : " + fmt.Sprint(i))
}
func HelloGoroutine(){
for i:=0;i<5;i++{
go func (j int){
hello(j)
}(i)
}
time.Sleep(time.Second)
}
输出结果是乱序,可以看出 goroutine 是并行的。
1.2 CSP (Communicating Sequential Processes)
提倡通过 通信共享内存 而不是通过共享内存而实现通信
Channel 相当于把 Goroutine 做了一个连接,就像是传输队列。遵循先入先出。能保证收发数据的顺序。 Channel 就是可以让一个 Goroutine 发特定的数据到另一个 Goroutine的一个通信机制。
当然 Go 也保留了通过共享内存实现通信,如右图所示,通过共享内存进行数据交换。需要通过 互斥量 Mutex 对内存进行加速,需要获取临界区的权限。但在这样的情况下,不同的Goroutine 容易发生 数据竞态的的问题。在一定条件下会影响性能。
扩展阅读(需要补充学习): 记得切换到 EN Wiki
1.3 Channel

make(chan 元素类型, [缓冲大小] )
-
无缓冲通道
make(chan int)发送和接收的 Goroutine同步化,又称为同步通道。解决同步问题的方式就是 使用带缓冲的通道。 -
有缓冲通道
make(chan int,2)通道的容量代表能存放多少元素,满了就阻塞。典型的生产-消费模型。
1.4 并发安全 Lock
1.5 WaitGroup
暴露了三个方法。
-
Add(delta int) :计数器+delta
-
Done() :计数器-1
-
Wait():阻塞直到计数器为0
就是一个 计数器, 开启协程 +1; 执行结束-1;主协程阻塞直到计数器为0。
Example:
func ManyGo() {
var wg sync.WaitGroup
wg.Add(5) // 计数器+5
for i := 0; i < 5; i++ {
wg.Add(1)
go func(j int) {
defer wg.Done() // 通过 done 对计数器-1
hello(j)
}(i)
}
wg.Wait() // 主协程堵塞
}
让我们回到最初多个协程带打印hello gorouting的例子,现在我们用watigroup实现协程的同步阻塞。如刚才说述,首先通过add方法,对计数器+5,然后开启协程,每个协程执行完后,通过done对计数器减少1,最后 wait 主协程阻塞,计数器为0 退出主协程。右边是最终的输出结果。
小结:
-
Goroutine
-
Channel
-
Sync (mutex waitGroup)
2. Go Package Manage (depend manage)
2.1 Go 依赖管理演进
GOPATH -> Go Vendor -> Go Module
-
不太环境(项目)依赖的版本不同
-
控制依赖库的版本
2.1.1 GOPATH
.
├── bin //项目编译的二进制文件
├── pkg //项目编译的中间产物,加速编译
└── src //项目源码
-
项目代码直接依赖 src 下的代码
-
go get 下载 latest package 到 src 目录下
弊端:
场景:A 和 B 依赖于某个 package 的不同版本
问题:无法实现 package 的多版本控制
2.1.2 Go Vendor
-
项目目录下增加 vendor 文件,所有依赖包副本形式放在
$ProjectRoot/vendor -
依赖寻址方式:vendor => GOPATH
通过每个项目引入一份依赖的副本,解决了多个项目需要同一个 package 依赖的冲突问题。 例,A项目的 vendor 下是 V1 版本,B项目的 vendor 下是 V2 版本。保证两个项目都能构建成功。
弊端:
如图项目 A 依赖 package B和C,而 Package B、C 又同时依赖于 Package D, 所以D 有 V1 和 V2 版本。通过 Vendor 没有办法很好的控制 V1 和 V2 的版本选择。一旦更新项目,可能出现依赖冲突,导致编译出错。
问题:
-
无法控制依赖的版本。
-
更新项目又可能出现依赖冲突,导致编译出错。
2.1.3 Go Module
-
通过
go.mod文件管理依赖包版本 -
通过
go get / go mod指令工具管理依赖包
目标: 定义版本规则和管理项目依赖关系
2.2 依赖管理三要素
-
- 配置文件,描述依赖
go.mod
- 配置文件,描述依赖
-
- 中心仓库管理依赖库
Proxy
- 中心仓库管理依赖库
-
- 本地工具
go get/mod
- 本地工具
2.3.1 依赖配置 go.mod
module exmaple/project/app // 依赖管理基本单元
//module github.com/<user_name>/<repo_name>/app
go 1.16 // 原生库
require( // 单元依赖
example/lib1 v1.0.2
example/lib2 v1.0.2 //indirect
example/lib3 v1.0.2-20190725025543-5a5f
example/lib5/v3 v3.0.2
example/lib6 v3.0.2+incompatible
)
依赖标识:[Module Path][Version/Pseudo-version]
从模块路径可以看出在哪找到该模块, github前缀表示可以从github 仓库找到。如果项目的子包想被单独引用,则需要通过单独的 init go.mod 文件进行管理。
下面是依赖的原生 SDK 版本。最下面是单元依赖,每个依赖单元用模块路径+版本号来唯一标示。
2.3.2 依赖配置 - version
语义化版本:
${MAJOR}.${MINOR}$.{PATCH}
-
V1.3.0
-
V2.3.0
基于 commit 的伪版本:
vx.0.0-yyyymmddhhmmss-abcdefgh1234
-
V0.0.0-20220401081311-c34098211231a
-
V1.0.0-20200401081311-10c340211231x
2.3.3 依赖配置 - indirect
A->B->C
-
A->B 直接依赖
-
A->C 间接依赖
没有直接导入的间接依赖模块, 就会标识为 indirect
2.3.4 依赖配置 - incompatible
-
主版本 V2+ 的模块会在模块路径添加 /vN 后缀。
-
对于没有 go.mod 文件且主版本 v2+ 的依赖,会
+incompatible
为了兼容性,做版本标识。
依赖配置会优先选择最低的兼容版本。
2.3.5 依赖分发 - 回源
表示依赖去哪里下载,如何下载的问题。
github是比较常见的代码托管系统平台,而Go Modules 系统中定义的依赖,最终可以对应到多版本代码管理系统中某一项目的特定提交或版本,这样的话,对于go.mod中定义的依赖,则直接可以从对应仓库中下载指定软件依赖,从而完成依赖分发。
但直接使用版本管理仓库下载依赖,存在多个问题。
-
无法保证构建确定性:软件作者可以直接代码平台增加/修改/删除 软件版本,导致下次构建使用另外版本的依赖,或者找不到依赖版本。
-
无法保证依赖可用性:依赖软件作者可以直接代码平台删除软件,导致依赖不可用;
-
大幅增加第三方代码托管平台 压力
2.3.5 依赖分发 - Proxy
Go Proxy 是一个服务站点,会缓冲源站中的软件内容,缓存版本不会改变,在源站删除软件后依旧可用。从而实现了更“immutability”和“available”(稳定和可靠)的依赖分发。使用了 Go Proxy,构建时直接从 Go Proxy 站点拉取。
类比项目中,下游无法满足我们上游的需求,使用 Go Proxy 或 适配器解决,如果解决不了就两层 Proxy。
PS: 类似 Linux Mirrors.
2.3.5 依赖分发 - 变量 GOPROXY
GOPROXY="https://proxy1.cn,https://proxy2.cn,direct"
Go Module 通过 环境变量来配置 GoProxy。
服务器站点URL列表,“direct” 表示源站(github, go.dev)。
查找依赖顺序示意图: Proxy1 => Proxy2 => Direct
类似设计缓存。
2.3.7 工具 - go get
-
@update:直接 go get,拉取 major 默认版本的最新提交 加不加一样
-
@none:删除依赖
-
@v1.1.2: 拉取特定的语义版本 tag
-
@23dfdd5: 拉取特定的 commit
-
@< breanch-name >: 某个分支最新的commit
2.3.8 工具 - go mod
-
init:初始化项目,创建 go.mod 文件。 每一个项目开始前必须的步骤。
-
Download: 下载模块到本地缓存。也就是 Pull.
-
tidy: 经常用到,根据项目增删需要的和不需要的依赖。
每次项目提交前,执行下 tidy。减少构建项目的时间。
02. 依赖管理三要素
小结:
-
Go 依赖管理演进
-
Go Module 依赖管理方案
4. 项目实践
4.1 需求分析
4.2 需求用例
4.3 ER图
4.4 分层结构
整体分为三层,repository数据层,service逻辑层,controoler视图层。 数据层关联底层数据模型,也就是这里的model,封装外部数据的增删改查,我们的数据存储在本地文件,通过文件操作拉取话题,帖子数据;数据层面向逻辑层,对service层透明,屏蔽下游数据差异,也就是不管下游是文件,还是数据库,还是微服务等,对service层的接口模型是不变的。 Servcie逻辑层处理核心业务逻辑,计算打包业务实体entiy,对应我们的需求,就是话题页面,包括话题和回帖列表,并上送给视图层。 Controller视图层负责处理和外部的交互逻辑,以view视图的形式返回给客户端,对于我们需求,我们封装json格式化的请求结果,api形式访问就好。
4.5 组件工具
-
Gin
-
Go Mod
-
go mod init
-
go get
-
4.6 Repository
4.6 Repository-index
4.6 Repository-查询
4.7 Service
代码流程编排
func (f *QueryPageInfoFlow) Do()(*PageInfo,error){
if err:= f.checkParam();err!=nil{
return nil,err
}
if err:=f.prepareInfo();err!=nil{
return nil,err
}
if err:=f.packPageInfo();err!=nil{
return nil,err
}
return f.pageInfo,nil
}