并发编程
Goroutine
协程:用户态,轻量级线程,栈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的实现
Channel
在goroutine之间通过channel进行连接
make(chan 元素类型,[缓冲大小])
- 无缓冲通道 make(chan int)
- 有缓冲通道 make(chan int,2)
通道缓冲大小代表可以存放的元素数量,满了会阻塞无法放入
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个,分别调用addWithLock和addWithoutLock函数。使用互斥锁保护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()会执行测试函数。
覆盖率
覆盖了代码中的部分占总代码量的比例
- 一般覆盖率:50%~60%,较高覆盖率80%+
- 测试分支相互独立、全面覆盖
- 测试单元粒度足够小,函数单一职责
mock
用于模拟依赖项或外部资源的行为,以便更好地控制和验证被测试代码的行为。
在测试环境中创建虚拟的对象或函数,来替代真实的依赖项,以达到以下目的:
- 隔离被测试单元:通过替代真实的依赖项,你可以确保测试的焦点仅仅在被测试的单元上,而不会受到依赖项的影响。
- 控制测试数据:使用 mock 对象,你可以提供预定义的数据或行为,以便模拟不同的场景,例如网络请求的成功或失败情况。
- 提高测试速度:通过使用 mock 对象,你可以避免与外部资源(例如数据库、网络服务)的真实交互,从而提高测试的执行速度。
部分mock包
基准测试
- 优化代码,需要对当前代码分析
- 内置的测试框架提供了基准测试的能力
规则
- 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
go get -u gorm.io/gorm