go工程进阶 | 青训营笔记

44 阅读4分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天

go 并发编程

协程 Goroutine

go并发编程核心: 协程 Goroutine

协程:用户态,轻量级线程,栈 KB 级别

线程:核心态,线程可以运行多个协程,栈 MB 级别

为一个函数启动 Goroutine 的关键字 go

//示例
package main

import (
	"fmt"
	"time"
)

func main() {
	HelloGoroutine()
}

func hello(i int) {
	fmt.Printf("hello goroutine: %v\n", i)
}
func HelloGoroutine() {
	for i := 0; i < 5; i++ {
		go func(j int) { 
			hello(j)
		}(i)
	}
	time.Sleep(time.Second)
}

协程间通信 Channel

​ go 提倡通过通信共享内存而不是通过共享内存而实现通信。这样可以避免竞态条件的发生,确保线程安全。Go 语言使用 Channel(通道)作为其内置的通信机制,以此来实现多个 goroutine 之间的通信,而不用担心竞态问题,提高运行性能。

Channel

make(chan elem_type, [size])

  • 无缓冲通道 make(chan elem_type)
  • 有缓冲通道 make(chan elem_type, size)

使用 Channel 实现一个生产者消费者模型:

func Square() {
	src, dest := make(chan int), 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 {
		fmt.Println(i)
	}
}

并发安全 Sync 包

Lock

​ Go 中的锁机制指的是 sync 包中的 Mutex,它的定义如下:

type Mutex struct { 
    state int32 
    sema  uint32 
}

​ Mutex 可以用来保护共享数据被多个 goroutine 同时访问时的正确性,它的原理是当一个 goroutine 请求一个 Mutex 时,它将会获得对应的锁,并将锁住共享数据,这样其他 goroutine 就无法访问该数据,直到当前 goroutine 释放了该锁,其他 goroutine 才可以获得该锁并访问共享数据。

//示例
var (
	x    int64
	lock sync.Mutex
)

func add() {
	x = 0
	for i := 0; i < 5; i++ {
		go addWithLock()
	}
	time.Sleep(time.Second)
	fmt.Printf("wtih lock: %v\n", x)
}

func addWithLock() {
	for i := 0; i < 2000; i++ {
		lock.Lock()
		x += 1
		lock.Unlock()
	}
}
WaitGroup

​ Go 提供了 WaitGroup 结构体来让我们知道是否所有的 goroutine 都完成了它们的工作,以及它们完成的先后顺序。 WaitGroup 是一个计数器,用来等待一组 goroutine 的完成。

​ 它有三个主要的方法:

  • Add:添加要等待的 goroutine 的计数,如果我们需要等待五个 goroutine,我们就可以使用 wg.Add(5)
  • Done:减少要等待的 goroutine 的计数,每个 goroutine 在它完成任务后都应该使用 wg.Done() 来减少计数。
  • Wait:阻塞当前 goroutine,直到要等待的所有 goroutine 都完成了任务。 一旦我们启动了所有的 goroutine,我们可以使用 wg.Wait () 方法来等待它们完成工作。当所有的 goroutine 都完成工作时,Wait 方法会返回。
//示例
func GroupWaitTest() {
	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()
}

依赖管理

GOPATH

​ Go 语言的 GOPATH 环境变量决定了 Go 语言开发时所依赖的目录位置,它是一个存储 Go 相关文件的文件系统路径,可以把它理解为 Go 语言的工作目录。GOPATH 变量所指定的目录中,存放着三个主要的目录:src、pkg 和 bin,其中 src 目录用来存放源代码,pkg 用来存放编译好的包文件,bin 用来存放可执行文件。GOPATH 还可以指定多个路径,用来存放不同的代码库。

  • 环境变量 $GOPATH
  • 项目代码直接依赖 src 下的代码
  • go get 下载最新版本的包到 src 目录下

弊端:无法实现 package 的多版本控制


Go Vendor

  • 项目目录下增加 vendor 文件,所有依赖包副本形式放在 $ProjextRoot/vendor
  • 依赖寻址方式: vendor => GOPATH

优势:

  1. Go Vendor 可以将依赖的文件放在项目根目录中,而不是放在 GOPATH 目录中,这样可以更好地控制依赖包的版本。
  2. Go Vendor 可以确保依赖包的完整性,而不会因为 GOPATH 中的依赖包被删除或更改而导致编译失败。
  3. Go Vendor 可以更好地跟踪依赖包的变更和更新,而不需要在 GOPATH 中搜索最新的版本。
  4. Go Vendor 可以更容易地分发项目,因为它可以将所有依赖包打包在一起,而不需要用户去其他的地方安装依赖包。

弊端:

  1. Go Vendor 是一个不可控的工具,比如可能会以你不知道的方式下载依赖包,并不能保证每次都使用同一版本的依赖包。
  2. Go Vendor 不支持自动更新,只能手动更新依赖包,这可能导致依赖包版本过时,破坏程序的稳定性。
  3. Go Vendor 不支持跨平台,如果代码要部署到其他平台,则需要重新安装依赖包,这会带来较大的麻烦。
  4. Go Vendor 无法控制依赖包的细节,只能下载整个依赖包,而不能定义具体的版本和功能。

Go Module

  • 通过 go.mod 文件管理依赖包版本
  • 通过 go get/mod 指令工具管理依赖包

依赖管理三要素:

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

测试

单元测试

规则:

  1. 所有文件以 _test.go 结尾
  2. 测试函数命名 func TestXxx(*testing T)
  3. 初始化逻辑放到 TestMain
//示例
func TestHelloTom(t *testing.T) {
	output := HelloTom()
	expectOutput := "Tom"
	if output != expectOutput {
		t.Errorf("Expected %s do not match actual %s", expectOutput, output)
	}
}

func HelloTom() string {
	return "Jerry"
}

通过第三方库来验证结果

package main

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

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

func HelloTom() string {
	return "Tom"
}

通过添加 cover 选项, go test xxx_test.go xxx.go --cover 可以输出单元测试的覆盖率


Mock 测试

​ 常用 Mock 组件 monkey

​ Mock测试是指在测试中创建和使用模拟对象或函数以模拟实际代码中的操作。它可以用于替换实际函数(打桩),以便在测试时使用特定的输入或状态,以便更轻松地验证特定的代码段。此外,它还可以用于模拟外部依赖项(如数据库,文件系统等),以模拟实际系统中可能出现的情况,而无需使用实际系统。使用Mock测试可以使测试更加简单,更快地完成,并且不会受到实际环境中的任何影响。

​ monkey 组件中的打桩与卸桩:

// Patch replaces a function with another
func Patch(target, replacement interface{}) *PatchGuard {
	t := reflect.ValueOf(target)
	r := reflect.ValueOf(replacement)
	patchValue(t, r)

	return &PatchGuard{t, r}
}
// Unpatch removes any monkey patches on target
// returns whether target was patched in the first place
func Unpatch(target interface{}) bool {
	return unpatchValue(reflect.ValueOf(target))
}

tip: 若使用的 go 版本默认启用了内联优化会导致打桩不生效,可以在测试命令后添加关闭内联的参数 go test -gcflags=-l

//示例
func TestHellTomWithMock(t *testing.T) {
   monkey.Patch(HelloTom, func() string {
      return "Tom"
   })
   defer monkey.Unpatch(HelloTom)
   res := HelloTom()
   assert.Equal(t, "Tom", res)
}

基准测试

​ Go 语言的基准测试(Benchmark)是一种特殊的单元测试,它的目的是使用多种不同的算法来测试函数的性能。它可以被用来测试函数的各个部分,也可以被用来测试不同算法之间的性能差异。Go 语言的基准测试使用 testing 包来实现,它提供了一个 Benchmark 函数,用于运行基准测试。使用基准测试,可以更好地了解程序的性能,并做出更好的设计决策。

​ 基准测试函数命名 func BenchmarkXxx(*testing T)

//示例
//串行执行测试
func BenchmarkHelloTom(b *testing.B) {
	for i := 0; i < b.N; i++ {
		HelloTom()
	}
}
//并行执行测试
func BenchmarkHelloTomParallel(b *testing.B) {
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			HelloTom()
		}
	})
}