Go进阶与依赖管理
协程
协程的使用
内存占用:
- 协程:协程的大小通常在KB级别,因为它们的实现更轻量,运行时只需要少量的堆栈空间。协程可以通过在同一线程中切换来管理多个任务,从而降低内存消耗。
- 线程:线程的大小一般在MB级别,线程在创建时会分配固定的堆栈空间,通常是1MB或更多。这导致在创建大量线程时,内存消耗会迅速增加。
切换开销:
- 协程:切换协程的开销相对较小,因为它们是在用户态进行切换,不需要进行上下文切换,这样可以提高性能。
- 线程:线程切换涉及内核态的上下文切换,开销相对较大,尤其是在大量线程并发时,性能下降更明显。
使用场景:
- 协程:适合I/O密集型任务,例如网络请求、文件读写等,因为它们可以有效利用空闲时间来执行其他协程。
- 线程:适合CPU密集型任务,需要并行执行多个计算密集型操作。
go
package main
import (
"fmt"
"time"
)
func hello(i int) {
println("hello goroutinu:" + fmt.Sprint(i))
}
func HelloGoRoutine() {
for i := 0; i < 5; i++ {
go func(j int) {
hello(j)
}(i)
}
time.Sleep(time.Second)
}
func main() {
HelloGoRoutine()
}
协程之间的通信
协程之间的通信通常建议采用"通过通信共享内存,而不是通过共享内存来实现通信"的方式
Channel
- 子协程发送0-9数字
- 子协程计算输入数字的平方
- 主协程输出最后的平方数
通道的定义:
src
通道用于传递源数据(0到9的整数)。dest
通道是一个带缓冲区的通道,缓冲区大小为3,用于存储计算后的平方值。
生产者协程:
- 第一个匿名协程负责将整数从0到9发送到
src
通道。在发送完所有数据后,使用defer close(src)
关闭src
通道,以通知消费者没有更多数据可供处理。
消费者协程:
- 第二个匿名协程从
src
通道中读取数据,计算每个数字的平方,并将结果发送到dest
通道。关闭dest
通道同样是通过defer close(dest)
来实现,以标识所有数据已经处理完毕。
go
package main
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)
}
}
func main() {
CalSquare()
}
并发安全Lock
对变量进行2000次+1操作,5个协程并发执行
- 共享数据的竞态条件 : 在
addWithoutLock
函数中,多个协程并行访问和修改共享变量x
,这可能导致数据竞争。在并发环境下,多个协程同时对x
进行加1操作,可能会产生未定义的行为,最终结果可能不等于预期。 - 互斥锁的使用 :
addWithLock
函数使用了sync.Mutex
互斥锁。在对x
进行修改时,先调用lock.Lock()
来获取锁,这确保在修改过程中其他协程无法同时访问x
。修改完成后,调用lock.Unlock()
释放锁。这样可以避免数据竞态,确保x
的值是安全的。
go
package main
import (
"sync"
"time"
)
var (
x int64
lock sync.Mutex
)
func addWithoutLock() {
for i := 0; i < 2000; i++ {
x += 1
}
}
func addWithLock() {
for i := 0; i < 2000; i++ {
lock.Lock()
x += 1
lock.Unlock()
}
}
func Add() {
x = 0
for i := 0; i < 5; i++ {
go addWithoutLock()
}
time.Sleep(time.Second)
println("addWithoutLock:", x)
x = 0
for i := 0; i < 5; i++ {
go addWithLock()
}
time.Sleep(time.Second)
println("addWithLock:", x)
}
func main() {
Add()
}
结果:
addWithoutLock: 6428 addWithLock: 10000
WaitGroup
在 Go 语言中,sync.WaitGroup
是一个用于协调多个协程(goroutines)并等待它们完成的同步原语
计数管理:
WaitGroup
可以用来跟踪正在运行的协程的数量。通过调用Add(n)
方法可以增加计数,表示要等待的协程数量。
协程完成的通知:
- 每个协程在完成其工作后,应该调用
Done()
方法来减少计数。通常使用defer wg.Done()
来确保在协程结束时自动调用Done()
。
等待所有协程完成:
- 在主协程或其他协程中,可以调用
Wait()
方法来阻塞当前协程,直到所有被跟踪的协程都调用了Done()
。这使得程序可以在所有并发任务完成后继续执行后续逻辑
go
package main
import "sync"
func hello(i int) {
println(i)
}
func ManyGoWait() {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func(j int) {
defer wg.Done()
hello(j)
}(i)
}
wg.Wait()
}
func main() {
ManyGoWait()
}
依赖管理
- 不同环境(项目)依赖的版本不同
- 控制依赖库的版本
Go Module
- 通过go.mod文件管理依赖包版本
- 通过go get/mod 指令工具管理依赖包
终极目标:定义版本规则和管理项目依赖关系
依赖配置Version:
分为语义化版本和基于Commit伪版本
依赖分发:
回源:相当于发送到GitHub等平台仓库
Proxy:增加一个代理,这个代理可以自己去GitHub,SVN等找
变量GOPROXY:自己定义服务站点url去找依赖
工具:
go get example.org/pkg:
- update 默认
- none 删除
- v1.1.2 tag版本
- 23dx x x 特定的commit
- master 分支最新Commit
go mod
- init 初始化
- download 下载模块
- tidy 增加/删除需要的依赖
测试
- 所有测试文件以
_test.go
结尾 func Testxxx(t *testing.T)
- 初始化逻辑放到
TestMain
中
go
func HelloTom() string {
return "CXK"
}
func TestHelloTom(t *testing.T) {
output := HelloTom()
excepted := "CXK1"
if output != excepted {
t.Errorf("output: %s, excepted: %s", output, excepted)
}
}
单元测试assert
go
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestHelloTom(t *testing.T) {
output := HelloTom()
excepted := "CXK"
assert.Equal(t, excepted, output)
}