Go并发优化与依赖管理 | 青训营;

109 阅读15分钟

Go并发优化与依赖管理

1.并发编程

Go语言是一种支持并发编程的编程语言,它通过轻量级的协程(goroutine)实现并发操作。协程是与线程相似但比线程更轻量级的并发单位,它由Go语言运行时管理,可以在相同的地址空间中并发执行。

1.1开启携程

在Go语言中,你可以使用关键字go来创建一个协程。通过在函数或方法调用前面加上go关键字,该函数或方法就会作为一个协程在后台并发执行。例如:

 package main
 ​
 import (
     "fmt"
     "time"
 )
 ​
 func main() {
     go sayHello()
     go countNumbers()
     
     // 主协程睡眠一段时间,以便给其他协程执行的时间
     time.Sleep(2 * time.Second)
 }
 ​
 func sayHello() {
     fmt.Println("Hello")
 }
 ​
 func countNumbers() {
     for i := 1; i <= 5; i++ {
         fmt.Println(i)
         time.Sleep(500 * time.Millisecond)
     }
 }

上面的示例中,sayHellocountNumbers函数被分别作为两个协程在后台并发执行。sayHello函数打印"Hello",countNumbers函数计数并打印数字1到5。主协程通过调用time.Sleep方法睡眠2秒钟,以保证所有协程有足够的时间完成执行。

通过使用协程,你可以高效地实现并发操作,而无需显式地创建和管理线程。Go语言的调度器会自动在多个线程上调度协程,并确保它们在适当的时间点进行切换。这使得编写并发程序变得更加简单和高效。

1.2 并发通信(通信->共享内存)channel通道

1.有缓冲 make(chan int)(同步化)

2.无缓冲 make(chan int,2)//两个缓冲空间

注意!:chan是并发安全的,可以保证协程顺序

  • 使用
 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()
 }
  • 效果
 C:\Users\Administrator\AppData\Local\Temp\GoLand___go_build_chan_main.exe
 0
 1
 4
 9
 16
 25
 36
 49
 64
 81
 ​
 Process finished with the exit code 0
 ​

1.3 并发安全-lock&unlock

在Go语言中,可以使用sync包提供的锁(Lock)来实现并发安全的数据访问。sync包提供了两种常用的锁类型:互斥锁(Mutex)和读写锁(RWMutex)。

互斥锁(Mutex)是最基本的一种锁,用于实现对临界区的独占访问。在需要保护共享资源的代码段之前,使用Lock方法获取锁,以防止其他协程同时访问该临界区。在访问完共享资源后,使用Unlock方法释放锁,允许其他协程获取它。

在下面的示例中,我们定义了一个全局变量counter,用来表示共享资源。在main函数中,我们创建了10个协程来并发地对counter进行递增操作。每个协程执行increment函数,在其中使用mutex.Lock()获取互斥锁,然后递增counter的值,最后使用mutex.Unlock()释放互斥锁。

通过使用互斥锁,我们确保了在任意时刻只有一个协程可以访问counter,从而避免了竞态条件(Race Condition)和数据不一致的问题。

除了互斥锁,sync包还提供了读写锁(RWMutex)来实现对共享资源的读写操作。读写锁允许多个协程并发地读取共享资源,但在进行写操作时,必须独占地获取锁。

你可以使用RWMutex类型的RLock方法获取读锁,使用RUnlock方法释放读锁;使用Lock方法获取写锁,使用Unlock方法释放写锁。

 package main
 ​
 import (
     "fmt"
     "sync"
 )
 ​
 var (
     counter = 0
     mutex   sync.Mutex
 )
 ​
 func main() {
     wg := sync.WaitGroup{}
 ​
     for i := 0; i < 10; i++ {
         wg.Add(1)
         go increment(&wg)
     }
 ​
     wg.Wait()
 ​
     fmt.Println("Final counter value:", counter)
 }
 ​
 func increment(wg *sync.WaitGroup) {
     defer wg.Done()
 ​
     mutex.Lock()
     defer mutex.Unlock()
 ​
     counter++
 }

1.4 waitGroup实现协程同步

sync.WaitGroup是Go语言中用于等待一组协程完成的同步原语。它的作用是阻塞主协程,直到所有的协程都完成任务。

使用WaitGroup需要以下步骤:

  1. 创建一个WaitGroup实例,并调用其Add方法来设置需要等待的协程数量。
  2. 每个协程执行任务前,需要在WaitGroup实例上调用Add(1)来增加等待的协程数量。
  3. 在每个协程的任务执行完成后,在WaitGroup实例上调用Done方法来减少等待的协程数量。
  4. 最后,在主协程中调用Wait方法,将会阻塞等待,直到等待的协程数量归零。

Add(n int) 计数器+n

Done() 计数器-1

Wait() 阻塞直到计数器为0

开启协程+1

完成协程-1

主协程阻塞到计数器为0

  • 使用

下面示例展示了如何使用sync.WaitGroup来精确控制并发协程的执行。

ManyGoWait函数中,我们创建了一个sync.WaitGroup变量wg,并调用wg.Add(5)来设置计数器的总数为5。这意味着我们希望等待5个协程完成任务。

然后,我们使用一个循环来创建5个协程,并在每个协程中调用hello函数。在匿名函数内部,我们使用defer wg.Done()来在协程完成任务后减少计数器的值。

通过使用wg.Wait(),主协程会阻塞等待,直到计数器的值归零,也就是所有协程都完成任务。这样确保了在主协程继续执行之前,所有的协程都已经执行完毕。

整个程序的执行流程如下:

  1. ManyGoWait函数中,首先调用wg.Add(5)将计数器值增加到5。
  2. 在循环中,创建5个协程,并在每个协程内部调用hello函数。
  3. 在匿名函数内部,使用defer wg.Done()减少计数器的值。
  4. 协程执行完成后,计数器的值递减。
  5. wg.Wait()会阻塞主协程,直到计数器的值归零。
  6. 主协程继续执行,输出结果。
 //使用waitGroup精准控制
 func ManyGoWait() {
     //使用waitGroup精准控制
     var wg sync.WaitGroup
     wg.Add(5) //打开计数器,并设置总的计数
     for i := 0; i < 5; i++ {
         go func(j int) {
             defer wg.Done() //延迟执行函数,先让计数器减去1
             hello(j)
         }(i)
     }
     wg.Wait() //阻塞主进程
 }
 C:\Users\Administrator\AppData\Local\Temp\GoLand___1go_build_main_go.exe
 hello goroutine:1
 hello goroutine:3
 hello goroutine:4
 hello goroutine:2
 hello goroutine:0
 ​

2.依赖管理 go-mod(module)

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

2.1 有哪些依赖管理

在Go语言中,依赖管理是用于管理和引用外部包(库)的机制。依赖管理旨在解决代码中使用到的外部包版本和依赖关系的管理问题。

Go语言有多种工具可以用于依赖管理,以下是两个常用的依赖管理工具:

  1. Go Modules: Go 1.11版本引入了Go Modules作为官方的依赖管理工具。Go Modules通过在项目根目录中的go.mod文件中记录模块的依赖关系和版本信息来管理依赖。可以使用go mod init命令初始化一个新的模块,并使用go get命令来获取依赖包。go.mod文件会自动维护和更新。

    通过使用Go Modules,可以轻松地管理项目的依赖关系,并且能够确保在构建项目时使用一致的依赖版本。可以使用go buildgo test等命令来构建和测试项目。

  2. Dep: dep是另一个常用的Go语言依赖管理工具,它提供了与Go Modules类似的功能。dep使用Gopkg.tomlGopkg.lock文件来记录项目的依赖关系和版本。dep可以通过dep init命令初始化项目,并使用dep ensure命令来获取和更新依赖包。

    dep具有一些高级功能,如支持嵌套的依赖关系、支持版本锁定、提供更好的错误消息等。虽然dep是一个好用的工具,但由于Go Modules的出现,Go官方将不再维护和推荐dep

2.2 go.mod文件

module example/project/app //依赖管理基本单元 go 1.16 //原生库版本 require( //单元依赖

example/lib1 v1.0.2 example/lib2 v1.0.0 // indirect example/lib3 v0.1.0-20190725025543-5a5fe074e612 example/lib4 v0.0.0-20180306012644-bacd9c7ef1dd // indirect example/lib5/v3 v3.0.2 example/lib6 3.2.0+incompatible

  • indirect 关键字(a->b->c a和c非直接依赖)
  • +incompatible 关键字(允许导入不兼容的版本)

2.3 版本规则

1.语义化版本

image-20230727110655974 V1.3.0 V2.3.0

2.基于 commit 伪版本

vx.0.0-yyyymmddhhmmss-abcdefgh1234

v0.0.0-20220401081311-c38fb59326b7 v1.0.0-20201130134442-10cb98267c6c

构建项目的时候会选择最低兼容版本

2.4 依赖分发

  • 其它方式

Go的依赖分发是指将一个Go程序及其依赖打包为一个可执行文件、库或模块,以便于分发和部署。

在Go语言中,依赖分发有以下几种常见的方式:

  1. 可执行文件分发:将Go程序及其所有依赖编译为一个独立的可执行文件,这样用户可以直接执行这个文件。这种方式最常见的是使用Go的交叉编译功能,可以在开发机器上编译出适用于不同操作系统和体系架构的可执行文件,然后将这些文件分发给用户。

    例如,使用以下命令编译一个可在Linux上运行的可执行文件:

     GOOS=linux GOARCH=amd64 go build -o myprogram
    

    这样生成的 myprogram 可执行文件可以在Linux系统上直接运行。

  2. 库分发:将Go程序打包为一个库,以供其他程序导入和使用。Go语言的包管理工具(如Go Modules)可以帮助我们管理和发布库。通过将库发布到公共或私有的代码仓库,其他开发者可以通过导入该库来使用它。

    例如,在Go Modules中,可以使用以下命令将库发布到公共代码仓库(如pkg.go.dev):

    go mod init github.com/username/mylibrary
    go mod tidy
    go build
    

    然后,其他开发者可以通过导入 github.com/username/mylibrary 来使用我们发布的库。

  3. 模块分发:如果项目由多个包组成,可以将整个项目打包为一个模块进行分发。使用Go Modules的go.mod文件可以指定项目的依赖关系和版本。

    例如,我们可以在项目的根目录中创建一个go.mod文件,并在其中指定项目的模块路径和依赖项,然后使用go buildgo run命令构建和运行项目。

    go mod init github.com/username/myproject
    go mod tidy
    go build
    

    这样可以将整个项目作为一个模块进行分发和使用。

无论是可执行文件、库还是模块分发,Go语言提供了便利的工具和机制来管理依赖分发,使得分发和部署成为一个相对简单和可靠的过程。根据项目的需求和实际情况,选择适合的分发方式。

  • proxy

GoProxy:GoProxy是Go官方维护的一个代理服务器,它是一个全球分布的镜像站点,能够缓存并提供Go模块的下载服务。使用GoProxy可以加速模块的下载,提高构建速度。可以通过设置GOPROXY=https://goproxy.cn来使用GoProxy作为代理。

在Go语言中,代理(Proxy)是一种用于帮助在网络上下载和缓存依赖包的工具。代理可以提高依赖包的下载速度、减轻镜像站点的负载,同时还可以解决由于网络访问限制或速度问题导致的依赖包下载失败的问题。

在Go的独立模块管理工具(如Go Modules)中,可以通过设置GOPROXY环境变量来指定代理服务器。代理服务器可以是一个本地的HTTP服务器,或是一个有权访问互联网的远程服务器。

2.5 go get

在Go语言中,go get是一个常用的命令,用于获取并安装远程包或模块。

go get的一般用法如下:

go get [flags] package/path

其中,package/path指定了要获取的远程包或模块的路径。

以下是go get常用的参数和示例:

  • -u:更新已安装的包到最新版本。

    go get -u github.com/example/package
    
  • -d:只下载包或模块,不安装。

    go get -d github.com/example/package
    
  • -t:同时获取测试所需的包。

    go get -t github.com/example/package
    
  • -v:显示详细的日志信息。

    go get -v github.com/example/package
    
  • -insecure:允许使用不安全的HTTPS连接。

    go get -insecure github.com/example/package
    
  • -tags:按标记过滤包。

    go get -tags="foo bar" github.com/example/package
    
  • -d -t:下载测试所需的包和包本身,但不安装。

    go get -d -t github.com/example/package
    

go get命令会自动从远程源代码仓库获取包或模块,并将其安装到$GOPATH目录(或Go Modules的默认缓存目录)下。如果没有设置GOPATH,则默认将其安装到$HOME/go目录下。

可以根据实际需求使用不同的go get参数来满足你的需求,例如更新已安装的包、只下载包而不安装、获取测试所需的包等。

2.6 go mod

在Go语言中,go mod是用于管理和维护Go模块(Go Modules)的命令行工具。Go Modules是Go 1.11版本引入的一种官方依赖管理机制,用于管理Go项目的依赖关系和版本控制。

以下是一些常用的go mod命令及其用法:

  • go mod init:用于初始化一个新的模块。在项目的根目录下执行该命令会创建一个新的go.mod文件。

    go mod init example.com/myproject
    
  • go mod tidy:根据项目的代码和导入的包来整理和清理模块的依赖关系,删除不再使用的依赖项,并将当前依赖项列表写入go.mod中。

    go mod tidy
    
  • go mod vendor:将项目的依赖项复制到项目的vendor目录下,以便于离线构建和版本锁定。

    go mod vendor
    
  • go mod download:下载项目的依赖包到本地缓存(如果还没有下载过)。

    go mod download
    
  • go mod graph:打印模块依赖项的图形表示,可以帮助查看和分析项目的依赖关系。

    go mod graph
    
  • go mod verify:验证依赖项是否符合已记录的摘要和哈希值,以确保依赖项的完整性。

    go mod verify
    
  • go mod edit:用于编辑go.mod文件,可以添加、更新或删除依赖项及其版本。

    go mod edit -require github.com/example/package@v1.2.3
    

这些命令可以帮助你在使用Go Modules时管理和维护项目的依赖关系。go mod init用于初始化模块,并生成go.mod文件,go mod tidy用于整理和清理依赖关系,go mod vendor用于复制依赖项到vendor目录等。

同时,go mod还支持一些其他的子命令,如go mod why用于查找特定依赖的原因,go mod graph用于打印依赖关系图等,可以根据需要使用这些命令来管理你的Go项目的依赖关系。

3.测试

3.1 单元测试

在Go语言中,单元测试是一种用于测试函数、方法和包的行为和正确性的测试方式。Go语言提供了内置的测试框架和工具,使得编写和运行单元测试变得非常简单。

以下是编写和运行Go语言单元测试的基本步骤:

  1. 创建测试文件:在与要测试的代码相同的目录下创建一个以_test.go结尾的文件,例如example_test.go

  2. 导入测试框架:在测试文件的开头导入Go的测试框架 import "testing"

  3. 编写测试函数:编写以 Test 开头的函数,并将参数设置为 *testing.T 类型。

    func TestAdd(t *testing.T) {
        // 测试逻辑
    }
    

    在测试函数中,使用 t.Run 和其他断言函数来测试代码的行为和结果。

  4. 运行测试:使用 go test 命令来运行测试。

    go test
    

    Go会自动查找当前目录和子目录中的测试文件,并执行其中的测试函数。执行过程中,会输出测试的结果和统计信息。

除了基本的步骤,还有一些常见的单元测试技巧和用法:

  • 使用 t.Errorft.Fatalf 来测试错误条件并打印错误信息。

    if result != expected {
        t.Errorf("expected %d but got %d", expected, result)
    }
    
  • 使用 t.Run 来组织和运行多个子测试。

    func TestMath(t *testing.T) {
        t.Run("Add", testAdd)
        t.Run("Subtract", testSubtract)
        // ...
    }
    
  • 使用 t.Helper 标记辅助函数,以指示该函数是测试辅助函数。

    func testAdd(t *testing.T) {
        t.Helper()
        // 测试逻辑
    }
    

Go的单元测试框架还提供了其他丰富的功能,如表格驱动测试、性能测试、覆盖率分析等。你可以根据实际需要来选择使用适合的测试方法和工具。

package main

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

func HelloTom() string {
   return "Tom"
}

func TestHelloTom(t *testing.T) {
   out := HelloTom()
   expectOutput := "Tom"
   assert.Equal(t, expectOutput, out)
}

func main() {
   var t testing.T
   TestHelloTom(&t)
}
package main

import (
   "bufio"
   "errors"
   "github.com/stretchr/testify/assert"
   "os"
   "strings"
   "testing"
)

func ReadFirstLine() string {
   open, err := os.Open("log")
   defer open.Close()
   if err != nil {
      errors.New("文件打开失败")
      return "error"
   }
   scanner := bufio.NewScanner(open)
   for scanner.Scan() {
      return scanner.Text()
   }
   return ""
}

func ProcessFirstLine() string {
   line := ReadFirstLine()
   destLine := strings.ReplaceAll(line, "11", "00")
   return destLine
}

func TestProcessFirstLine(t *testing.T) {
   firstLine := ProcessFirstLine()
   assert.Equal(t, "line00", firstLine)
}

func main() {
   var t testing.T
   TestProcessFirstLine(&t)
}

3.2 基准测试

在Go语言中,你可以使用内置的基准测试框架来对代码的性能进行基准测试。基准测试可以帮助你比较不同实现的性能,并进行优化。

以下是一个基准测试的示例:

package main

import (
	"strings"
	"testing"
)

func BenchmarkConcatenateStrings(b *testing.B) {
	str1 := "Hello"
	str2 := "World"
	for n := 0; n < b.N; n++ {
		_ = str1 + str2
	}
}

func BenchmarkJoinStrings(b *testing.B) {
	strs := []string{"Hello", "World"}
	for n := 0; n < b.N; n++ {
		_ = strings.Join(strs, "")
	}
}

在上面的代码中,我们定义了两个基准测试函数BenchmarkConcatenateStringsBenchmarkJoinStrings。这些函数以Benchmark开头,并接受一个*testing.B类型的参数,表示基准测试的上下文。

在每个基准测试函数中,我们使用b.N循环来运行测试代码。b.N表示基准测试的迭代次数,框架会根据测试的执行时间和精度自动确定循环的次数。

在示例中,BenchmarkConcatenateStrings基准测试函数测试了字符串连接操作的性能,而BenchmarkJoinStrings测试了使用strings.Join函数连接字符串的性能。

要运行基准测试,可以在终端中使用go test命令,并使用-bench标志指定要运行的基准测试函数的匹配模式。例如,要运行上述示例中的所有基准测试函数,你可以执行以下命令:

go test -bench=.

执行命令后,框架会运行基准测试函数,并输出相关的性能统计信息,如每次迭代的耗时、平均每次迭代的耗时等。

注意,基准测试的结果可能会受到多个因素的影响,如硬件性能、操作系统等。因此,建议在相同的环境中运行基准测试以进行比较。还可以使用-benchtime标志调整每次基准测试的时间,以获得更准确的结果。

基准测试是优化代码性能的重要工具,它可以帮助你发现潜在的性能问题并优化代码实现。

4. 下载gin依赖

go get gopkg.in/gin-gonic/gin.v1@v1.3.0