go语言进阶与依赖管理详解 | 青训营

124 阅读3分钟

1.go语言进阶

1.1 Goroutine

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

image.png

创建协程,快速打印 image.png

  • Goroutine 是 Go 语言中的并发执行的机制,它可以让你以一种简洁的方式编写并发程序。Goroutine 可以看作是一种轻量级的线程,在 Go 的运行时环境中,它会被放置在一个线程中执行。

  • 创建 Goroutine 非常简单,只需要在函数或方法调用前加上 go 关键字即可。例如:

go doSomething()
  • 上述代码会在一个新的 Goroutine 中执行 doSomething() 函数,而主程序会继续执行后续的代码,不会等待新的 Goroutine 完成。

  • Goroutine 之间可以通过通道(Channel)进行通信和同步。通道是一种类型安全的、并发安全的数据结构,用于 Goroutine 之间的数据传递。你可以使用 make 函数来创建一个通道,然后通过发送和接收操作来进行数据的传递。

下面是一个简单的示例,演示了如何使用 Goroutine 和通道:

package main

import "fmt"

func doSomething(c chan string) {
    // 执行一些操作

    // 发送结果到通道
    c <- "完成任务"
}

func main() {
    // 创建通道
    c := make(chan string)

    // 启动 Goroutine 执行任务
    go doSomething(c)

    // 从通道接收结果
    result := <-c
    fmt.Println(result)
}
  • 在上述示例中,通过创建一个通道 c,然后在 doSomething() 函数中将任务的结果发送到通道中。主程序中使用 <- 运算符从通道中接收结果,并将结果打印出来。

  • 需要注意的是,Goroutine 的调度是由 Go 运行时自动管理的,因此你无法保证 Goroutine 的执行顺序。但是,你可以使用同步原语如互斥锁(Mutex)或等待组(WaitGroup)来控制 Goroutine 的执行顺序和同步操作。

1.2CSP

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

image.png

  • CSP(Communicating Sequential Processes)是一种并发编程模型,它起源于Tony Hoare在1978年提出的一篇论文。Go 语言的并发编程模型就是基于 CSP 的理念设计的。

  • CSP 的核心思想是通过通信来实现并发。在 CSP 中,并发的执行单元被称为进程(Process),进程之间通过通道(Channel)进行通信。每个进程都是独立的,拥有自己的执行空间,通过在通道上发送和接收消息来进行进程间的数据交换。进程之间的通信是同步的,发送和接收操作会阻塞,直到对方准备好或缓冲区有空间。

  • Go 语言的 Goroutine 和 Channel 就是基于 CSP 模型设计的。Goroutine 可以看作是轻量级的进程,而 Channel 则是进程间的通道。

下面是一个使用 Channel 实现 CSP 的简单示例:

package main

import "fmt"

func process1(ch chan string) {
    // 执行一些操作

    // 发送消息到通道
    ch <- "Message from process 1"
}

func process2(ch chan string) {
    // 接收通道消息
    msg := <-ch
    fmt.Println("Message received:", msg)
}

func main() {
    // 创建通道
    ch := make(chan string)

    // 启动两个进程
    go process1(ch)
    go process2(ch)

    // 等待进程执行完毕
    fmt.Scanln()
}
  • 在上述示例中,我们创建了两个进程 process1 和 process2,它们通过通道 ch 进行消息的发送和接收。进程 process1 向通道发送一条消息,进程 process2 则从通道接收该消息,并将其打印出来。

  • 需要注意的是,通道的发送和接收操作都是阻塞的,这意味着发送和接收操作会等待对方的准备或缓冲区的空间。这种同步机制可以确保并发操作的正确性和一致性。

  • CSP 提供了一种简洁、直观的方式来处理并发编程,通过通过通信来共享数据,而不是通过共享数据来通信,可以避免许多常见的并发问题。Go 语言的 Goroutine 和 Channel 结合了 CSP 模型的思想,并提供了高效、安全的并发编程解决方案。

1.3 Channel

make(chan 元素类型,【缓冲大小】)

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

image.png

A 子协程发送0~9 B 子协程计算输入数字的平方 主协程输出最后的平方数 image.png

  • 在 Go 语言中,通道(Channel)是一种并发安全的数据结构,用于 Goroutine 之间的通信和同步。通道可以传递特定类型的数据,确保发送和接收操作的同步性。

  • 通道可以被创建、发送和接收数据,并且具有阻塞特性。阻塞特性使得 Goroutine 在发送或接收数据时可以进行同步,即发送操作会等待接收操作的准备或缓冲区的空间,接收操作会等待发送操作的数据。

  • 创建通道可以使用 make 函数,指定通道中元素的类型。例如,创建一个字符串类型的通道:

ch := make(chan string)
  • 发送操作使用 <- 运算符将数据发送到通道中。例如,将字符串 “Hello” 发送到通道:
ch <- "Hello"
  • 接收操作使用 <- 运算符从通道中接收数据。例如,从通道中接收字符串,并将其赋值给变量 msg
msg := <- ch
  • 通道可以设置缓冲区,通过在创建通道时指定缓冲区的大小。缓冲区可以存储一定数量的元素,使发送操作非阻塞,直到缓冲区被填满。当缓冲区为空时,接收操作会阻塞,直到有数据可接收。

以下是一个简单示例,演示了如何使用通道进行传递和同步数据:

package main

import "fmt"

func worker(ch chan string) {
    // 接收数据
    msg := <-ch
    fmt.Println("接收到数据:", msg)

    // 发送数据
    ch <- "World"
}

func main() {
    // 创建通道
    ch := make(chan string)

    // 启动 Goroutine
    go worker(ch)

    // 发送数据
    ch <- "Hello"

    // 接收数据
    result := <-ch
    fmt.Println("接收到数据:", result)
}
  • 在上述示例中,我们创建了一个通道 ch,然后启动了一个 Goroutine worker,在其中接收来自通道的数据,并发送数据到通道。在主程序中,我们先发送字符串 “Hello” 到通道中,然后再接收从 Goroutine 发送过来的数据。这样就实现了在不同 Goroutine 之间的数据传递和同步。

  • 需要注意的是,通道的发送和接收操作会阻塞,因此在使用通道时需要确保发送和接收的 Goroutine 之间配合良好,以避免死锁和其他并发问题。

  • 通道是 Go 语言中强大的并发原语,可以有效地进行 Goroutine 之间的数据传递和同步,提供了一种安全、简洁的方式来处理并发编程。

1.4并发安全Lock

image.png

image.png

  • 在 Go 语言中,实现并发编程中的互斥访问和同步操作可以使用锁(Lock)机制。通过将关键代码块放置在锁的保护下,可以确保同一时间只有一个 Goroutine 在执行该代码块,从而避免数据竞争和并发问题。

  • Go 语言提供了内置的互斥锁(Mutex)和读写锁(RWMutex)来实现锁的功能。

  • 互斥锁(Mutex)用于保护临界区代码的访问,确保同一时间只有一个 Goroutine 可以进入临界区。以下是一个示例:

package main

import (
	"fmt"
	"sync"
)

var counter int
var mutex sync.Mutex

func increment() {
	mutex.Lock()         // 获取锁
	counter = counter + 1 // 修改共享变量
	mutex.Unlock()       // 解锁
}

func main() {
	var wg sync.WaitGroup

	for i := 0; i < 10; i++ { // 启动 10 个 Goroutine
		wg.Add(1)
		go func() {
			increment()
			wg.Done()
		}()
	}

	wg.Wait() // 等待所有 Goroutine 完成

	fmt.Println("Counter:", counter)
}
  • 在上述示例中,我们使用互斥锁来保护对 counter 变量的访问。increment() 函数中,使用 mutex.Lock() 获取锁,确保同一时间只有一个 Goroutine 可以执行 counter = counter + 1 的操作。在操作完成后,使用 mutex.Unlock() 释放锁。

  • 读写锁(RWMutex)用于更细粒度的锁定,允许多个 Goroutine 同时对共享资源进行读取,但只能有一个 Goroutine 进行写入或修改操作。以下是一个示例:

package main

import (
	"fmt"
	"sync"
)

var data map[string]string
var rwMutex sync.RWMutex

func readData(key string) {
	rwMutex.RLock() // 获取读锁
	value := data[key]
	rwMutex.RUnlock() // 释放读锁
	fmt.Println("Read data:", value)
}

func writeData(key, value string) {
	rwMutex.Lock() // 获取写锁
	data[key] = value
	rwMutex.Unlock() // 释放写锁
}

func main() {
	data = make(map[string]string)

	// 写入数据
	go writeData("key1", "value1")
	go writeData("key2", "value2")

	// 读取数据
	for i := 0; i < 10; i++ {
		go readData("key1")
	}

	// 等待所有 Goroutine 完成
	fmt.Scanln()
}
  • 在上述示例中,我们使用读写锁来对 data 进行读写操作。readData() 函数中,使用 rwMutex.RLock() 获取读锁,允许多个 Goroutine 同时执行读取操作。writeData() 函数中,使用 rwMutex.Lock() 获取写锁,确保同一时间只有一个 Goroutine 可以执行写入操作。

    通过使用锁机制,我们可以安全地处理并发编程中的共享资源,避免数据竞争和并发问题。需要根据具体的应用场景选择合适的锁机制,保证并发程序的正确性和性能。

1.5WaitGroup

计数器:开启协程+1,执行结束-1,主协程阻塞直到计数器为0。 image.png

image.png

  • 在 Go 语言中,WaitGroup 是一种同步原语,用于等待一组 Goroutine 执行完成。WaitGroup 维护着一个计数器,用于追踪还未完成的 Goroutine 的数量。当计数器的值变为 0 时,表示所有的 Goroutine 都已经执行完成。

WaitGroup 提供以下三个方法:

    1. Add(delta int):向计数器添加指定的值,表示有若干个 Goroutine 需要等待完成。
    1. Done():减少计数器的值,表示一个 Goroutine 已经完成。
    1. Wait():阻塞当前 Goroutine,直到计数器的值变为 0。

下面是一个示例,演示了如何使用 WaitGroup:

package main

import (
	"fmt"
	"sync"
	"time"
)

func worker(id int, wg *sync.WaitGroup) {
	defer wg.Done() // Goroutine 完成时减少计数器

	fmt.Printf("Worker %d starting\n", id)
	time.Sleep(time.Second) // 模拟耗时操作
	fmt.Printf("Worker %d done\n", id)
}

func main() {
	var wg sync.WaitGroup

	for i := 1; i <= 5; i++ {
		wg.Add(1) // 新增一个 Goroutine,计数器加一
		go worker(i, &wg)
	}

	wg.Wait() // 阻塞,直到计数器变为 0
	fmt.Println("All workers done")
}
  • 在上述示例中,我们创建了 5 个 Goroutine,并且每个 Goroutine 都会模拟一个耗时操作。在主程序中,我们使用 WaitGroup 来等待所有的 Goroutine 执行完成。

  • 首先,我们通过 wg.Add(1) 将计数器加一,表示有一个 Goroutine 需要等待。然后,使用 go worker(i, &wg) 启动 Goroutine 执行任务,其中的 &wg 是传递 WaitGroup 指针,用于修改计数器。

  • 在 worker 函数中,使用 defer wg.Done() 来表示当前 Goroutine 执行完成时,减少计数器的值。在执行完耗时操作后,我们可以看到计数器逐渐减少,直到变为 0。

最后,使用 wg.Wait() 阻塞主 Goroutine,直到计数器变为 0,即所有的 Goroutine 都执行完成。然后输出 “All workers done”。

通过使用 WaitGroup,我们可以有效地等待多个 Goroutine 完成,实现并发任务的同步。这对于需要等待一组并发操作完成的场景非常有用。

2.依赖管理

2.1 Go依赖管理演进

2.1.1 GOPATH

在早期的 Go 语言版本中,GOPATH 是一个特定的环境变量,它指定了你的工作空间的根目录。GOPATH 环境变量包含了三个重要的子目录:

    1. src:用于存放 Go 语言的源代码文件(.go 文件)。
    1. pkg:用于存放编译后生成的包对象文件(.a 文件)。
    1. bin:用于存放可执行文件。

通过设置 GOPATH 环境变量,Go 语言工具链可以在指定的工作空间中找到源代码、编译和安装包、构建并生成可执行文件。

2.1.2 Go Vendor

Go Vendor 是 Go 语言中用于管理项目依赖的工具之一。它旨在将项目所依赖的第三方包(包括其源代码和相关资源)直接复制到项目的 vendor 目录下,以实现项目的独立性和可移植性。

要使用 Go Vendor,首先确保你的 Go 版本在 1.5 或更高。然后,请按照以下步骤进行操作:

    1. 在项目的根目录下创建一个 vendor 目录:mkdir vendor
    1. 使用 Go 的依赖管理工具(如 go mod、dep 等)下载你所需的第三方包:go mod download 或 dep ensure
    1. 将下载的包复制到 vendor 目录下:go mod vendor 或 dep ensure -vendor-only

这样,你的项目就会在 vendor 目录下创建了相应的包结构,包含了你所需要的第三方包的源代码和资源文件。在构建和部署时,Go 会首先查找并使用 vendor 目录中的包。如果 vendor 目录中没有对应的包,Go 会继续在全局的 GOPATH 中查找。

2.1.3 Go Module

Go Module 是 Go 语言自1.11版本引入的官方依赖管理解决方案。它提供了一种跨平台、版本化的依赖管理方式,可以更好地管理项目的依赖关系和版本控制。

使用 Go Module 进行依赖管理的主要步骤如下:

    1. 初始化模块:在项目的根目录下,执行 go mod init 命令来初始化一个新的模块。这会创建一个 go.mod 文件来记录模块的依赖关系和版本信息。
    1. 添加依赖:使用 go get 命令来添加项目所需的依赖包,例如 go get github.com/example/package。Go Module 会自动下载并管理相应的依赖版本。
    1. 管理版本:Go Module 使用语义化的版本号来管理依赖。可以使用 go list -m all 命令查看当前项目的所有依赖及其版本。
    1. 构建和测试:使用 go build 或 go test 命令来构建和测试项目。Go Module 会根据 go.mod 文件中指定的版本信息自动下载依赖,并且确保依赖的一致性。

2.2依赖管理三要素

1.配置文件,描述依赖

  • 配置文件(Configuration File):配置文件用于描述项目的依赖关系和版本信息。在 Go 语言中,常用的配置文件是 go.mod。它存储了项目的模块信息,包括模块名称、依赖包的版本要求、替代模块等。配置文件提供了一个明确的方式来定义和管理项目的依赖。

2.中心仓库管理依赖库

  • 中心仓库(Central Repository):中心仓库是位于互联网上的集中存储依赖库的地方。在 Go 语言中,默认的中心仓库是 Go Modules Proxy(proxy.golang.org)。中心仓库管理着大量的开源依赖库,并提供了版本控制、下载和缓存功能,使得开发者可以方便地引入和更新依赖库。

3.本地工具

  • 本地工具(Local Tools):本地工具是用于帮助管理依赖的工具程序。在 Go 语言中,常用的本地工具包括 go命令本身和其他辅助工具(如 go mod、go get、dep 等)。这些工具提供了命令行接口和操作方式,用于初始化模块、添加依赖、更新依赖、解决依赖冲突等操作,以满足项目的依赖管理需求。

2.3依赖配置

2.3.1依赖配置- go.mod

  • 依赖配置 - go.mod:go.mod 是 Go 语言项目中的配置文件,用于描述项目的依赖关系和版本信息。其中包含了模块名称、依赖包的版本要求、替代模块、块等信息。通过编辑 go.mod 文件,可以添加、更新或删除项目的依赖。使用指令 go mod init 来初始化一个新的模块,并通过 go get 命令自动更新 go.mod 文件中的依赖。

2.3.2依赖配置 -version

  • 依赖配置 - version:在 go.mod 文件中,可以使用 version 指令来声明依赖包的版本要求。可以使用准确的版本号(如 v1.2.3),也可以使用版本范围(如 >v1.0.0)。通过指定版本要求,可以确保项目使用的依赖符合预期的稳定性和兼容性要求。

2.3.3依赖配置 -indirect

  • 依赖配置 - indirect:indirect 指令用于声明项目的间接依赖。当一个依赖是由其他直接依赖所引入时,可以使用 indirect 指令将其标记为间接依赖。这样,在项目构建和版本解析过程中,依赖管理工具会忽略这些间接依赖的版本要求,以减少冲突和复杂性。

2.3.4依赖配置 -incompatible

  • 依赖配置 - incompatible:incompatible 指令用于声明与当前模块不兼容的依赖。当存在与当前模块不兼容的包版本时,可以使用 incompatible 指令将其标记为不兼容。这样,在进行依赖解析和版本选择时,依赖管理工具会避免选择这些不兼容的版本。

2.3.5依赖分发-回源

2.3.6依赖分发-变量GOPROXY

  • 在 Go 语言中,依赖分发是指将项目所需的依赖包分发给开发者或工程师使用的过程。 Go 语言提供了一些工具和机制来帮助进行依赖分发,并确保依赖的可用性和稳定性。 依赖分发的一种方式是通过变量 GOPROXY 来配置全局代理。GOPROXY 表示 Go 语言执行 go get 命令时用于获取依赖包的代理地址。通过设置 GOPROXY 的值为合适的代理地址,可以使得 go get 命令从指定的代理服务器获取依赖包,而不是直接从默认的中心仓库或其他渠道获取。这样可以保证依赖包的可用性和下载速度。

2.3.7工具-go get

  • go get:go get 是 Go 语言的一个命令行工具,用于下载和安装依赖包。通过执行 go get 命令,可以从指定的代理地址或其他渠道下载所需的依赖包,并将其保存在 GOPATH 或 go.mod 指定的目录中。

2.3.8工具-go mod

  • go mod:go mod 是 Go 语言的官方依赖管理工具。它可以通过执行 go mod tidy 命令来自动解析 go.mod 文件中声明的依赖关系,并下载相应的依赖包。这样,开发者只需维护一个 go.mod 文件即可,不需要手动下载和安装依赖包。