Go语言进阶与依赖管理

Go语言进阶与依赖管理

Go语言进阶

一、Goroutine 协程

并发与并行概念

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

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

Go 语言可以充分发挥多核优势,高效运行。它能够很好地利用现代计算机的多核架构,将任务合理分配到不同的核心上进行并行处理

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

  • 线程与协程关系:线程处于内核态,一个线程可以跑多个协程。线程为协程提供了运行的环境,协程则在这些线程之上进行具体的任务执行。通过这种方式,既可以利用线程与操作系统内核的紧密联系来确保程序的稳定性和资源获取能力,又能借助协程的轻量级特性实现高效的并发任务处理。

二、Channel 通道

CSP(Communicating Sequential Processes)

  • 核心思想:提倡通过通信共享内存而不是通过共享内存而实现通信。这种理念与传统的通过共享内存来实现线程间通信的方式有所不同。在 CSP 模型中,重点在于各个进程(或协程)之间通过明确的通信机制来传递数据和协调工作,而不是直接去共享和操作同一块内存区域,这样可以有效避免因共享内存带来的诸如数据竞争、死锁等并发问题。
1
make(chan 元素类型,[缓冲大小])

无缓冲通道 make(chan int)
有缓冲通道 make(chan int, 2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func CalSquare() {
// 通道创建
src := make(chan int)
dest := make(chan int, 3)
// 第一个匿名函数(数据生成)
go func() {
defer close(src)
for i := 0; i < 10; i++ {
src <- i
}
}()
// 第二个匿名函数(数据处理与传输)
go func() {
defer close(dest)
for i := range src {
dest <- i * i
}
}()
// 主流程(结果输出)
for i := range dest {
println(i)
}
}

在实际复杂的并发场景中,如果协程的启动和执行出现延迟或异常,可能会导致通道没有按预期关闭,进而可能引发一些难以排查的问题,比如程序阻塞等待永远不会到来的数据等。

三、Sync 同步

1. 并发安全Lock

为什么需要并发安全锁?

当多个并发执行的单元(如线程、协程)同时访问和修改共享资源(比如全局变量、共享数据结构等)时,可能会出现以下问题:

  • 数据竞争:不同的执行单元可能在同一时刻对同一数据进行读写操作,导致最终结果不可预测。例如,两个线程同时对一个整数变量进行自增操作,如果没有适当的同步机制,最终变量的值可能不是预期的正确结果。
  • 状态不一致:在涉及多个相关数据的修改时,如果并发操作没有协调好,可能会使共享资源处于一种不一致的状态。比如,对一个链表进行插入和删除操作时,若并发执行可能导致链表结构混乱

在并发编程中,并发安全是一个重要的概念,确保在多个线程或协程同时访问和操作共享资源时不会出现数据不一致、错误或其他意外行为。Lock(锁)是实现并发安全的一种常用机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import (
"sync"
"time"
)

var (
x int64
lock sync.Mutex
)

func addWithLock() {
for i := 0; i < 2000; i++ {
lock.Lock()
x += 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("WithoutLock: ", x)

x = 0
for i := 0; i < 5; i++ {
go addWithLock()
}
time.Sleep(time.Second)
println("WithLock: ", x)
}

func main() {
Add()
}

image-20241115130758673

2. WaitGroup

WaitGroup 是 Go 语言标准库中用于处理协程同步的一个重要结构体,它提供了一种简洁有效的方式来确保主协程能够等待所有被启动的协程完成其任务后再继续执行后续操作,避免了主协程过早结束而导致其他协程的执行被中断或未完成任务的情况。

1
2
3
4
5
6
7
8
9
10
11
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()
}

计数器,开启协程+1; 执行结束-1; 主协程阻塞直到计数器为0

Add: 计数器+delta

Done: 计数器-1

Wait: 阻塞直到计数器为0

Go语言依赖管理

不同环境(项目)依赖的版本不同,控制依赖库的版本

一、Go 依赖管理的演进

1. GOPATH

在 GOPATH 模式下,主要依赖环境变量 $GOPATH 来组织项目相关的文件。它包含三个重要的子目录:

  • bin:用于存放项目编译生成的二进制文件。当执行go build等编译命令后,生成的可执行文件会被放置在此目录下,方便直接运行这些程序。
  • pkg:这里存储的是项目编译的中间产物,比如对象文件等。其存在的主要目的是为了加速后续的编译过程。在后续再次编译项目时,如果相关的中间产物没有发生变化,就可以直接使用这些已有的中间产物,而无需重新进行完整的编译流程,从而节省了编译时间。
  • src:是存放项目源码的地方,同时也是项目代码直接依赖其他代码的来源地。所有的项目代码以及所依赖的外部包的源码都需要放在这个目录下。例如,如果项目依赖了某个开源包,那么需要通过go get命令将该包的源码下载到src目录下相应的位置,然后项目才能正常引用这些代码。

弊端:无法实现package的多版本控制
也就是说,只能有一个版本的该package存在于src目录中,这就可能导致不同项目或不同模块对同一package的不同版本需求无法同时满足,进而影响项目的正常开发和运行。

2. GO Vendor

  • 项目目录结构变化:在这种模式下,项目目录下会增加一个vendor文件(通常是一个目录),所有依赖包会以副本形式放在$ProjectRoot/vendor目录下。这里的$ProjectRoot指的是项目的根目录。
  • 依赖寻址方式:其依赖寻址方式变为先在vendor目录中查找所需的依赖包,如果在vendor目录中找不到,再去GOPATH中查找

弊端:无法控制依赖的版本。更新项目又可能出现依赖冲突,导致编译出错。
例如,当更新项目中的某个模块,该模块又依赖于其他一些包,而这些包在vendor目录中的版本可能与更新后的模块不兼容,从而引发编译问题。

3. Go Module

  • 通过 go.mod 文件管理依赖包版本
  • 通过 go get/go mod 指令工具管理依赖包

终极目标: 定义版本规则和管理项目依赖关系

依赖管理三要素

  1. 配置文件,描述依赖 go.mod
  2. 中心仓库管理依赖库 Proxy
  3. 本地工具 go get/mod

二、Go Module 依赖管理方案

1. 依赖配置

1. go.mod

go.mod文件是Go语言项目中依赖管理的核心文件,它明确了项目的依赖关系以及相关的版本信息等。

1
2
3
4
5
6
7
8
9
10
11
module example/project/app	//依赖管理基本单元 
go 1.16 // 原生库
require(
// 依赖标识: [Module Pathl[Version/Pseudo-versionl
example/lib1 v1.0.2 // 单元依赖
example/lib2 v1.0.0 // indirect 非直接依赖
example/lib3 v.1.0-20190725025543-5a5fe074e612
example/lib4 v0.0.0-20180306012644-bacd9c7ef1dd // indirect
example/lib5/v3 v3.0.2 // 主版本2+模块会在模块路径增加/vN 后缀,
example/lib6 v3.2.0+incompatible // 对于没有 go.mod 文件并且主版本2+的依赖,会+incompatible
)

2. version

语义化版本

主版本号:次版本号:补丁版本号

${MAJOR}.${MINOR}.${PATCH}
V1.3.0
V2.3.0

基于 commit 伪版本

版本前缀-时间戳-十二位hash码前缀
vx.0.0-yyyymmddhhmmss-abcdefgh1234
v0.0.0-20220401081311-c38fb59326b7
v1.0.0-20201130134442-10cb98267c6c

2. 依赖图

选择最低的兼容版本:

在构建项目时,会根据依赖图来确定各个依赖库的版本选择。依赖图展示了项目所有依赖库之间的相互关系,包括直接依赖和间接依赖。为了确保项目的兼容性和稳定性,通常会选择每个依赖库的最低兼容版本。这意味着在满足项目功能需求的基础上,尽量采用版本较低且与其他依赖库能够良好兼容的版本,以减少因版本更新带来的潜在兼容性问题。

3. 依赖分发

回源:直接使用版本管理仓库下载依赖

存在问题:

  • 无法保证构建稳定性:增加/修改/删除软件版本
  • 无法保证依赖可用性:删除软件
  • 增加第三方压力:代码托管平台负载问题

Proxy方式及配置

  • Proxy机制:为了解决回源方式存在的问题,引入了Proxy(代理)机制。Proxy作为一个中间服务站点,缓存了大量的依赖库版本,当项目需要下载依赖时,可以先从Proxy获取。
  • 配置变量GOPROXY:通过设置环境变量GOPROXY来指定代理服务的站点URL列表

变量GOPROXY

1
GOPROXY="https://proxy1.cn, https://proxy2.cn,direct"

服务站点URL列表,“direct”表示源站
这里指定了两个代理服务站点https://proxy1.cnhttps://proxy2.cn,最后的direct表示源站。当从代理站点无法获取到所需的依赖库时,会尝试直接从源站(即版本管理仓库)获取。这样既可以利用代理站点缓存的优势来提高下载速度和保证依赖的可用性,又能在代理站点无法满足需求时,通过源站获取到所需的依赖库。

4. 工具

  • go get:这是Go语言中用于获取依赖库的一个重要工具。它可以根据go.mod文件中的依赖声明,从指定的源(如版本管理仓库或代理站点)获取相应版本的依赖库,并更新go.mod文件中的版本信息等。例如,如果在go.mod文件中新增了一个依赖声明example/lib7 v2.0.0,执行go get example/lib7 v2.0.0就会去获取example/lib7这个库的v2.0.0版本,并将其添加到项目的依赖体系中,同时更新go.mod文件相关内容。

    image-20241115141445408

  • go mod:是一组用于管理项目依赖关系的工具命令集合。它包括了诸如go mod update(用于更新所有依赖库到最新版本)、go mod verify(用于检查所有依赖库之间的兼容性)等多种命令,通过这些命令可以对项目的依赖关系进行全面的管理,确保项目在依赖方面的稳定性和兼容性。

    image-20241115141521718


Go语言进阶与依赖管理
https://leaf-domain.gitee.io/2025/03/22/go/Go语言进阶与依赖管理/
作者
叶域
发布于
2025年3月22日
许可协议