跳到主要内容

Go进阶与依赖管理

协程

协程的使用

内存占用

  • 协程:协程的大小通常在KB级别,因为它们的实现更轻量,运行时只需要少量的堆栈空间。协程可以通过在同一线程中切换来管理多个任务,从而降低内存消耗。
  • 线程:线程的大小一般在MB级别,线程在创建时会分配固定的堆栈空间,通常是1MB或更多。这导致在创建大量线程时,内存消耗会迅速增加。

切换开销

  • 协程:切换协程的开销相对较小,因为它们是在用户态进行切换,不需要进行上下文切换,这样可以提高性能。
  • 线程:线程切换涉及内核态的上下文切换,开销相对较大,尤其是在大量线程并发时,性能下降更明显。

使用场景

  • 协程:适合I/O密集型任务,例如网络请求、文件读写等,因为它们可以有效利用空闲时间来执行其他协程。
  • 线程:适合CPU密集型任务,需要并行执行多个计算密集型操作。
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

  1. 子协程发送0-9数字
  2. 子协程计算输入数字的平方
  3. 主协程输出最后的平方数

通道的定义

  • src 通道用于传递源数据(0到9的整数)。
  • dest 通道是一个带缓冲区的通道,缓冲区大小为3,用于存储计算后的平方值。

生产者协程

  • 第一个匿名协程负责将整数从0到9发送到 src 通道。在发送完所有数据后,使用 defer close(src) 关闭 src 通道,以通知消费者没有更多数据可供处理。

消费者协程

  • 第二个匿名协程从 src 通道中读取数据,计算每个数字的平方,并将结果发送到 dest 通道。关闭 dest 通道同样是通过 defer close(dest) 来实现,以标识所有数据已经处理完毕。
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个协程并发执行

  1. 共享数据的竞态条件: 在 addWithoutLock 函数中,多个协程并行访问和修改共享变量 x,这可能导致数据竞争。在并发环境下,多个协程同时对 x 进行加1操作,可能会产生未定义的行为,最终结果可能不等于预期。
  2. 互斥锁的使用addWithLock 函数使用了 sync.Mutex 互斥锁。在对 x 进行修改时,先调用 lock.Lock() 来获取锁,这确保在修改过程中其他协程无法同时访问 x。修改完成后,调用 lock.Unlock() 释放锁。这样可以避免数据竞态,确保 x 的值是安全的。
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()。这使得程序可以在所有并发任务完成后继续执行后续逻辑
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
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

import (
"github.com/stretchr/testify/assert"
"testing"
)

func TestHelloTom(t *testing.T) {
output := HelloTom()
excepted := "CXK"
assert.Equal(t, excepted, output)
}