1.3.工程实践
1.3.1.协程 线程
协程(Goroutine)
- 概念:协程是用户态的轻量级线程,由Go运行时管理和调度。
- 创建:通过在函数调用前加上 go 关键字来创建协程。例如:
go myFunction()
3. 调度:Go运行时负责协程的调度,包括创建、销毁、挂起和恢复。协程之间的切换开销非常小,通常只需要几十纳秒。 4. 栈大小:协程的栈大小是动态调整的,初始栈大小很小(通常是2KB),根据需要自动增长和缩小。 5. 内存占用:协程的内存占用非常低,因此可以同时创建成千上万个协程而不会消耗过多的内存。
线程(Thread)
- 概念:线程是操作系统提供的并发执行单元,由操作系统管理和调度。
- 创建:创建线程通常需要调用操作系统的API,例如在C语言中使用 pthread_create。
- 调度:线程的调度由操作系统内核负责,涉及上下文切换和调度策略。线程之间的切换开销较大,通常需要微秒级的时间。
- 栈大小:线程的栈大小通常是固定的,一般为几MB,具体取决于操作系统的默认设置。
- 内存占用:线程的内存占用较高,因此创建大量线程会消耗大量的内存资源。
1.3.2.通信 共享
传统并发模型:通过共享内存进行通信
在传统的并发模型中,多个线程或进程通过共享同一块内存区域来交换数据。这种模型的优点是可以直接访问共享数据,但缺点也非常明显:
- 同步问题:多个线程同时访问共享内存时,需要使用锁(mutex)来防止数据竞争(data race)。锁的管理和使用非常复杂,容易出错。
- 死锁和活锁:锁的不当使用可能导致死锁(deadlock)和活锁(livelock),使得程序难以调试和维护。
- 性能瓶颈:频繁的锁操作会带来额外的开销,影响程序的性能。
Go并发模型:通过通信共享内存
Go语言通过引入协程(goroutine)和通道(channel)来解决这些问题,其核心思想是“通过通信共享内存”。具体来说,Go鼓励开发者通过通道来传递数据,而不是直接访问共享内存。这种方式有以下几个优点:
- 简化同步:通道提供了一种同步机制,确保发送方和接收方在数据传递时同步。开发者不需要手动管理锁,减少了同步错误的可能性。
- 避免数据竞争:通过通道传递数据,每个协程只拥有自己的数据副本,避免了数据竞争的问题。
- 提高代码可读性和可维护性:通过通信的方式,代码逻辑更加清晰,易于理解和维护。
- 高效的并发:Go运行时负责协程的调度和管理,协程之间的通信开销非常小,可以高效地处理大量并发任务。
1.3.3.创建channel
基本语法
- 创建无缓冲通道:
ch := make(chan T)
其中 T 是通道中传递的数据类型。无缓冲通道在发送和接收操作之间是同步的,即发送方会阻塞直到接收方准备好接收数据。
- 创建带缓冲通道:
ch := make(chan T, buffer_size)
其中 buffer_size 是通道的缓冲区大小。带缓冲通道在缓冲区未满时发送操作不会阻塞,接收操作在缓冲区非空时也不会阻塞。
1.3.4.WaitGroup
sync.WaitGroup 是 Go 语言标准库中的一个同步原语,用于在并发程序中等待多个协程(goroutines)完成。WaitGroup 通过增加计数器来跟踪需要等待的协程数量,当所有协程完成时,计数器归零,等待的主协程可以继续执行。
主要功能
- 增加计数器:使用
Add方法增加需要等待的协程数量。 - 减少计数器:使用
Done方法减少计数器,表示一个协程已经完成。 - 等待所有协程完成:使用
Wait方法阻塞当前协程,直到计数器归零。
基本用法
//创建 WaitGroup 实例:
var wg sync.WaitGroup
//增加计数器:
wg.Add(1)
//启动协程:
go func() {
// 协程的逻辑
defer wg.Done() // 协程完成时减少计数器
// 执行任务
}()
//等待所有协程完成:
wg.Wait()
1.3.5.go mod
go mod 是 Go 语言从 1.11 版本开始引入的模块管理系统,旨在解决依赖管理和版本控制的问题。go mod 提供了一种标准化的方法来管理项目的依赖关系,确保项目在不同环境下的构建一致性。以下是 go mod 的详细介绍,包括其基本概念、常用命令和使用示例。
基本概念
- 模块(Module) :
-
- 一个模块是一个包含
go.mod文件的 Go 代码集合。 go.mod文件定义了模块的名称、依赖关系及其版本。
- 一个模块是一个包含
- go.mod 文件:
-
go.mod文件是模块的元数据文件,记录了模块的名称、依赖关系及其版本。- 通常位于项目的根目录。
- 版本管理:
-
go mod使用语义版本号(Semantic Versioning)来管理依赖版本。- 依赖版本可以是具体的版本号(如
v1.2.3)、版本范围(如^1.2.3)或分支(如master)。
- 缓存:
-
go mod会将下载的模块缓存到本地的GOPATH/pkg/mod目录,以提高后续构建的速度。
常用命令
- 初始化模块:
go mod init [module_path]
-
- 创建一个新的
go.mod文件,指定模块的路径。 - 例如:
go mod init example.com/myproject
- 创建一个新的
- 下载依赖:
go mod download
-
- 下载
go.mod文件中列出的所有依赖模块。
- 下载
- 更新依赖:
go mod tidy
-
- 清理未使用的依赖,并添加缺失的依赖。
- 也可以使用
go get -u更新依赖到最新版本。
- 查看依赖树:
go mod graph
-
- 显示模块的依赖关系图。
- 验证模块:
go mod verify
-
- 验证模块的完整性和一致性。
- 替换依赖:
go mod edit -replace old_module_path=new_module_path
-
- 临时替换依赖模块的路径,常用于开发过程中测试本地修改的依赖。
在go.mod文件中:
- 不兼容版本:
// incompatible注释表示该依赖的版本与当前项目的Go版本不兼容。通常出现在使用+incompatible标记的版本中,这些版本通常是在语义版本号之外的特殊版本。
- 间接依赖:
// indirect注释表示该依赖是间接引入的,即它不是由你的项目直接导入的,而是由你的项目依赖的其他模块引入的。
1.3.6.go get
go get 是 Go 语言中的一个命令,用于下载和安装外部模块(库或工具)。go get 不仅可以下载模块,还可以将其添加到当前项目的依赖中,并在必要时编译和安装模块。以下是 go get 的详细说明和常见用法。
基本用法
- 下载模块:
go get <module_path>
例如,下载 github.com/gorilla/mux 模块:
go get github.com/gorilla/mux
2. 下载并安装工具:
go get <tool_path>
例如,下载并安装 golang.org/x/tools/cmd/goimports 工具:
go get golang.org/x/tools/cmd/goimports
详细说明
- 下载模块:
-
go get会从指定的模块路径下载模块,并将其添加到当前项目的go.mod文件中。- 如果当前项目不在模块模式下(即没有
go.mod文件),go get会创建一个临时的模块,并将模块添加到vendor目录中。
- 安装工具:
-
go get可以下载并安装 Go 工具,这些工具通常是一些命令行工具,用于辅助开发和测试。- 安装的工具会被放置在
GOPATH/bin目录中,确保GOPATH/bin在你的PATH环境变量中,以便可以直接运行这些工具。
- 更新模块:
-
- 使用
-u选项可以更新模块到最新版本:
- 使用
go get -u <module_path>
-
- 使用
-u=patch选项可以更新模块到最新的补丁版本:
- 使用
go get -u=patch <module_path>
4. 指定版本:
-
- 使用
@version语法可以指定下载特定版本的模块:
- 使用
go get <module_path>@v1.2.3
-
- 例如,下载
github.com/gorilla/mux的v1.8.0版本:
- 例如,下载
go get github.com/gorilla/mux@v1.8.0
5. 下载所有依赖:
-
- 如果你只想下载当前项目的所有依赖,而不安装任何新的模块,可以使用
go mod download命令:
- 如果你只想下载当前项目的所有依赖,而不安装任何新的模块,可以使用
go mod download
1.3.7.单元测试
Go 语言内置了强大的单元测试框架,使得编写和运行测试变得非常简单。单元测试是软件开发中的一种重要实践,用于验证代码的各个部分是否按预期工作。Go 的测试框架主要通过 testing 包来实现,下面详细介绍 Go 中的单元测试。
1.基本概念
测试文件
- 测试文件的命名规则:测试文件的名称必须以
_test.go结尾,例如example_test.go。 - 测试文件通常与被测试的源文件放在同一个目录中。
测试函数
- 测试函数的命名规则:测试函数的名称必须以
Test开头,并且接受一个*testing.T类型的参数。 - 例如:
func TestAdd(t *testing.T)
2.编写测试函数
基本结构
package yourpackage
import (
"testing"
)
func TestAdd(t *testing.T) {
// 调用被测试的函数
result := Add(2, 3)
// 预期结果
expected := 5
// 检查结果是否符合预期
if result != expected {
t.Errorf("Add(2, 3) = %d, want %d", result, expected)
}
}
*testing.T:用于单个测试函数中,提供报告测试结果和失败信息的方法。*testing.M:用于控制整个测试包的运行,通常在自定义测试主函数中使用,以便在测试前后执行初始化和清理操作