Go工程进阶 | 青训营

46 阅读5分钟

并发编程

Goroutine

image-20230514144633558

协程:用户态,轻量级线程,栈KB级别: 线程:内核态,线程跑多个协程,栈MB级别。

package main
​
import (
    "fmt"
    "time"
)
​
func out(i int) {
    fmt.Println("routine :", i)
}
​
func main() {
    for i := 0; i < 10; i++ {
        // 使用 go 关键字创建一个协程,协程内部调用 out() 函数,参数为 i
        // 这里的 go 关键字就是创建协程的语法,紧跟着 go 关键字的就是要在协程中运行的方法
        go func(j int) {
            out(j)
        }(i) // 注意这里的(i),我们将循环变量i作为参数传递给协程,保证每个协程拿到的是正确的值
    }
    time.Sleep(time.Second)
    // 主函数等待一秒钟,以便给协程执行提供足够的时间
    // 实际开发中,我们往往使用更为可靠的方式来等待所有协程执行完毕,如 sync.WaitGroup
}

CSP

Communicating Sequential Processes,go提倡通过通信共享内存,即Channel的实现

image-20230514215537047

Channel

在goroutine之间通过channel进行连接

make(chan 元素类型,[缓冲大小])

  • 无缓冲通道 make(chan int)
  • 有缓冲通道 make(chan int,2)

image-20230514212957018

通道缓冲大小代表可以存放的元素数量,满了会阻塞无法放入

package main
​
import "fmt"func main() {
    // 创建一个无缓冲的int类型channel,用于传递整数
    src := make(chan int)
    // 创建一个有缓冲的int类型channel,缓冲区大小为3,用于存储和传递整数
    dest := make(chan int, 3)
​
    // 启动一个协程
    go func() {
        // 当协程函数退出时,关闭src channel
        defer close(src)
        // 循环10次,将0-9的整数依次发送到src channel中
        for i := 0; i < 10; i++ {
            src <- i
        }
    }()
​
    // 启动另一个协程
    go func() {
        // 当协程函数退出时,关闭dest channel
        defer close(dest)
        // 从src channel中接收数据,直到src channel关闭
        for i := range src {
            // 将接收到的数据平方后发送到dest channel中
            dest <- i * i
        }
    }()
​
    // 在主函数中,从dest channel中接收数据,直到dest channel关闭
    for i := range dest {
        // 打印接收到的数据
        fmt.Println(i)
    }
}

defer是go中一种延迟调用机制,defer后面的函数只有在当前函数执行完毕后才能执行,将延迟的语句按defer的逆序进行执行,也就是说先被defer的语句最后被执行,最后被defer的语句,最先被执行,通常用于释放资源。

Lock

在通过共享内存实现通信时,存在线程安全问题,需要加锁。

package main
​
import (
    "fmt"
    "sync"
    "time"
)
​
var (
    x    int64
    lock sync.Mutex
)
​
func addWithLock() {
    for i := 0; i < 2000; i++ {
        lock.Lock()
        x++
        lock.Unlock()
    }
}
​
func addWithoutLock() {
    for i := 0; i < 2000; i++ {
        x++
    }
}
​
func main() {
    x = 0
    for i := 0; i < 5; i++ {
        go addWithLock()
    }
    time.Sleep(time.Second)
    fmt.Println(x) //10000
    x = 0
    for i := 0; i < 5; i++ {
        go addWithoutLock()
    }
    time.Sleep(time.Second)
    fmt.Println(x) //7269
}

sync.Mutex类型的lock变量是一个互斥锁。互斥锁在多线程和多协程编程中经常用来保护共享资源,以防止在并发访问时发生数据竞态。当一个协程已经获得了互斥锁,其他试图获取锁的协程就会被阻塞,直到锁被释放。

addWithLock函数中,每次增加x的值前都会先获得锁,然后再释放锁。这样可以保证在增加x的值的过程中,没有其他的协程可以同时修改x的值。这就是所谓的“原子操作”,即在操作过程中不会被其他协程中断。

然后在main函数中,先后启动了两组协程,每组5个,分别调用addWithLockaddWithoutLock函数。使用互斥锁保护x的操作可以得到正确的结果(10000),而没有使用互斥锁的操作则可能会得到错误的结果(例如7269)。这是因为在没有使用互斥锁的情况下,多个协程可能会同时读取到x的值,然后同时增加x的值,导致一些增加操作被“覆盖”了。这就是所谓的数据竞态。

WaitGroup

package main
​
import (
    "fmt"
    "sync"
)
​
func main() {
    group := sync.WaitGroup{}
    group.Add(5)
    for i := 0; i < 5; i++ {
        go func(j int) {
            defer group.Done()
            fmt.Println("goroutine:", j)
        }(i)
    }
    group.Wait()
}

sync.WaitGroup 用于等待一组并发操作完成。它提供了一种同步机制,使得主线程能够等待所有的 goroutine 执行完毕后再继续执行。

代码中的 group := sync.WaitGroup{} 创建了一个空的 WaitGroup 实例。然后,group.Add(5) 调用将等待组的计数器设置为 5。这意味着主线程将等待 5 个 goroutine 完成后再继续执行。

接下来的 for 循环创建了 5 个 goroutine。每个 goroutine 都会执行一个匿名函数,并通过参数 j 接收一个循环变量的值。在匿名函数内部,defer group.Done() 表示在当前 goroutine 执行完毕时调用 Done() 方法,以通知等待组计数器减一。

在每个 goroutine 内部,通过 fmt.Println("goroutine:", j) 打印了一条信息。

最后,group.Wait() 语句将阻塞主线程,直到等待组的计数器归零。也就是说,它将一直等待直到所有的 goroutine 都调用了 Done() 方法,表示它们已经完成。一旦等待组的计数器为零,Wait() 方法会解除阻塞,主线程继续执行。

依赖管理

官方包管理工具 go mod

设置国内代理

go env -w GOPROXY=https://proxy.golang.com.cn,https://mirrors.aliyun.com/goproxy/,https://goproxy.bj.bcebos.com/,https://goproxy.cn,direct

命令

go mod init

  • 意思: 初始化一个新的模块。
  • 用法: go mod init

go get

  • 意思: 下载依赖项或添加新的依赖项。

  • 用法: go get @

  • 附加参数:

    • -d: 只下载依赖项,不安装。
    • -t: 同时下载测试所需的依赖项。
    • -u: 更新已安装的依赖项到最新版本。
    • -insecure: 允许从非安全(HTTP)源下载依赖项。
    • -u=patch: 更新到最新的补丁版本。

go mod tidy

  • 意思: 清理不再使用的依赖项,并验证go.mod文件。

  • 用法: go mod tidy

  • 附加参数:

    • -v: 显示执行详细日志。

go build

  • 意思: 构建项目或指定的包。

  • 用法: go build [build flags] [packages]

  • 附加参数:

    • -mod : 设置依赖项的下载模式(例如:readonly、vendor)。
    • -modfile : 指定替代的go.mod文件路径。
    • -mod=readonly: 强制只读模式,不允许下载依赖项。

go run

  • 意思: 编译并运行指定的Go文件。
  • 用法: go run [build flags] [arguments]
  • 附加参数: 与go build命令相同。

go list

  • 意思: 列出模块中的包。

  • 用法: go list

  • 附加参数:

    • -m: 显示主模块或所有模块的信息。
    • -u: 显示可升级的依赖项。
    • -versions: 显示可用的版本列表。
    • -json: 以JSON格式输出结果。

go mod download

  • 意思: 命令用于下载模块到本地缓存。这个命令会将项目依赖的模块下载到本地缓存中,以便在没有网络连接的情况下也能构建和测试这些模块。

  • 用法: go mod download [模块名@版本]

  • 附加参数:

    • 模块名@版本: 指定要下载的模块的路径和版本。如果没有指定,该命令将会下载所有在go.mod文件中声明的依赖模块。
    • -json: 以JSON格式输出模块版本详细信息。

go.mod示例文件

module demo // 模块名称go 1.20 // Go 语言的版本
​
require ( // 导入的依赖项
    github.com/gin-gonic/gin v1.7.2 // 依赖的包名和版本
    github.com/go-sql-driver/mysql v1.5.0 
)
​
exclude ( // 排除的版本
    github.com/gin-gonic/gin v1.7.1 // 要排除的包名和版本
)
​
replace ( // 替换的模块路径
    github.com/go-sql-driver/mysql => github.com/myfork/mysql v1.5.0 // 要替换的包名,以及替换后的包名和版本
)
​
  • module 指定了模块的名字。
  • go 指定了Go语言的版本。
  • require 列出了模块的依赖项,每一项后面都跟着版本号。
  • exclude 可以排除某个包的特定版本,如果该版本有问题,可以通过这个指令避免使用。
  • replace 可以替换依赖项的导入路径,这在你需要使用自己的分支或fork版本时会很有用。

go.mod通常是由Go工具自动生成和更新的,不需要手动去编辑。如果需要添加或升级依赖项,可以使用go get命令,Go工具会自动更新go.mod文件。

测试

  • 所有测试文件以_test.go结尾

单元测试

规则

  • func TestXxx(t *testing.T)
  • 初始化逻辑放在TestMain中

gos.go

package main
​
// 定义一个简单的函数,它将两个整数相加
func add(a int, b int) int {
    return a + b
}

gos_test.go

package main
​
import (
    "fmt"
    "os"
    "testing" // 引入testing模块,它提供了测试框架
)
​
var expected int// 测试add函数
func TestAdd(t *testing.T) { // 测试函数名以Test开头,参数为*testing.T类型
    //expected := 4       // 预期的结果
​
    actual := add(2, 4) // 实际的结果,调用我们的add函数if actual != expected { // 判断实际结果是否等于预期结果
        t.Errorf("Add(2, 4) = %d; expected = %d", actual, expected) // 如果不等于,输出错误信息
    }
    //assert.Equal(t, 6, add(2, 4), "结果错误") // 使用assert.Equal来断言结果是否正确
​
}
​
func TestMain(m *testing.M) {
    // 执行任何需要在所有测试之前进行的初始化操作
    fmt.Println("在所有测试之前执行的初始化操作")
    expected = 6
    // 运行测试,并获取测试结果的退出码
    exitCode := m.Run()
​
    // 执行任何需要在所有测试之后进行的清理操作
    fmt.Println("在所有测试之后执行的清理操作")
​
    // 退出测试程序,并将测试结果的退出码返回给调用者
    os.Exit(exitCode)
}

可以使用断言做判断。使用 assert.Equal 函数来代替之前的 if 语句。assert.Equal 接受四个参数,第一个是 *testing.T 对象,第二个是期望值,第三个是实际值,第四个是当测试失败时输出的错误信息。如果实际值和期望值不相等,assert.Equal 会使测试失败并输出错误信息。

go get github.com/stretchr/testify/assert	

在运行测试时,如果有,会先执行TestMain(),可以做初始化和清理操作,中间的m.Run()会执行测试函数。

覆盖率

覆盖了代码中的部分占总代码量的比例

image-20230519222552257

  • 一般覆盖率:50%~60%,较高覆盖率80%+
  • 测试分支相互独立、全面覆盖
  • 测试单元粒度足够小,函数单一职责

mock

用于模拟依赖项或外部资源的行为,以便更好地控制和验证被测试代码的行为。

在测试环境中创建虚拟的对象或函数,来替代真实的依赖项,以达到以下目的:

  1. 隔离被测试单元:通过替代真实的依赖项,你可以确保测试的焦点仅仅在被测试的单元上,而不会受到依赖项的影响。
  2. 控制测试数据:使用 mock 对象,你可以提供预定义的数据或行为,以便模拟不同的场景,例如网络请求的成功或失败情况。
  3. 提高测试速度:通过使用 mock 对象,你可以避免与外部资源(例如数据库、网络服务)的真实交互,从而提高测试的执行速度。

部分mock包

github.com/stretchr/te…

github.com/golang/mock

github.com/vektra/mock…

github.com/bouk/monkey

基准测试

  • 优化代码,需要对当前代码分析
  • 内置的测试框架提供了基准测试的能力

规则

  • func BenchmarkXxx(b *testing.B)
package main

import (
	"fmt"
	"math"
	"testing"
	"time"
)

// 基准测试函数
func BenchmarkSquareRoot(b *testing.B) {
	//Timer是计时器
	b.ResetTimer()
	//时间太短的话计算不出每次操作的时间
	b.N = 100
	b.StopTimer()

	b.StartTimer()
	// 在基准测试函数中执行待测的代码
	for i := 0; i < b.N; i++ {
		// 计算平方根
		_ = math.Sqrt(float64(i))
		time.Sleep(1)
	}
	b.StopTimer()

}

func BenchmarkSquare(b *testing.B) {
	// 在基准测试函数中执行待测的代码
	for i := 0; i < b.N; i++ {
		// 计算平方
		_ = i * i
	}
}

func main() {
	// 运行基准测试,并输出结果
	result := testing.Benchmark(BenchmarkSquareRoot)
	fmt.Println("BenchmarkSquareRoot:", result)

	result = testing.Benchmark(BenchmarkSquare)
	fmt.Println("BenchmarkSquare:", result)
}

编写并行基准测试:

package main

import (
	"fmt"
	"math"
	"testing"
)

func BenchmarkSquareRoot(b *testing.B) {
	b.RunParallel(func(pb *testing.PB) {
		// 在并行基准测试中执行待测的代码
		for pb.Next() {
			// 计算平方根
			_ = math.Sqrt(float64(b.N))
		}
	})
}

func main() {
	// 运行并行基准测试,并输出结果
	result := testing.Benchmark(BenchmarkSquareRoot)
	fmt.Println("BenchmarkSquareRoot:", result)
}

GORM

gorm.io/zh_CN/docs/

go get -u gorm.io/gorm