1.Go语言进阶
-从并发编程的视角带大家了解Go高性能的本质
1.1 Goroutine 协程
函数前加上go进行开启协程
func HelloGoRoutine() {
for i := 0;i < 5; i++ {
//这是一个匿名函数
go func(j int) {
hello(j)
}(i)
}
}
fun hello(i int){
fmt.print(i)
}
1.2 CSP
在Go语言中,CSP代表通信顺序进程(Communicating Sequential Processes)。CSP是一种并发编程模型,用于在并发程序中管理不同进程(goroutines)之间的通信和同步。它由计算机科学家Tony Hoare于1978年首次提出,旨在帮助程序员设计并发系统,并提供简单可靠的方法来处理并发问题。
核心思想是通过消息传递来实现不同的goroutines之间的通信,而不是通过共享内存。每个goroutine都是一个独立的执行单元,通过通道(channel)来发送和接收消息。通道是用于在不同goroutines之间传递数据的管道。
CSP的基本概念包括:
- Goroutines(进程):Go语言中的轻量级线程,允许同时执行多个函数,从而实现并发。
- 通道(Channel):通道是用来在goroutines之间传递数据的管道。发送和接收操作是阻塞的,这意味着在发送和接收数据之前,goroutine会等待通道的操作完成。
- 发送(Send):将数据从一个goroutine发送到通道。
- 接收(Receive):从通道接收数据,并将其从通道中删除。
- 同步(Synchronization):通过通道来同步不同的goroutines,确保它们按预期顺序执行。
1.3 Channel 通道
语法
make(==chan== 元素类型 , [缓冲大小])
- 无缓冲通道 make(chan int)
- 有缓冲通道 make(chan int, 2)
package main
/*
Channel
*/
func main() {
CalSquare()
}
func CalSquare() {
//无缓冲队列
src := make(chan int)
//有缓冲队列
dest := make(chan int, 3)
//生产者 产生数据
go func() {
//defer 会在函数退出时调用 资源关闭
defer close(src)
//生产10个数据
for i := 0; i < 10; i++ {
src <- i
}
}()
//消费者 消费数据
go func() {
//defer 会在函数退出时调用 资源关闭
defer close(dest)
//消费数据 数据进行平方处理
for i := range src {
dest <- i * i
}
}()
//对处理后的结果进行打印
for i := range dest {
println(i)
}
}
1.4 Lock 并发安全
Go语言中,可以通过使用锁来实现并发安全。
Go标准库中提供了一个sync包,其中包含了常用的锁类型,其中最常用的是sync.Mutex和sync.RWMutex。
课程示例代码
package main
import (
"sync"
"time"
)
func main() {
Add()
}
var (
X int64
Lock sync.Mutex
)
func addWithLock() {
for i := 0; i < 2000; i++ {
Lock.Lock()
X += 1
Lock.Unlock()
}
}
func addWithoutLock() {
for i := 0; i < 2000; i++ {
X += 1
}
}
func Add() {
X = 0
for i := 0; i < 5; i++ {
go addWithoutLock()
}
time.Sleep(time.Second)
println("WithoutLock:", X)
X = 0
for i := 0; i < 5; i++ {
go addWithLock()
}
time.Sleep(time.Second)
println("WithLock:", X)
}
sync.Mutex
互斥锁是最简单的锁类型,它可以用于保护临界区,确保在同一时间只有一个goroutine可以访问被锁定的资源。
package main
import (
"fmt"
"sync"
)
var (
counter int
mutex sync.Mutex
)
func increment() {
mutex.Lock() // 加锁
counter++
mutex.Unlock() // 解锁
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
increment()
wg.Done()
}()
}
wg.Wait()
fmt.Println("Counter:", counter)
}
sync.RWMutex
读写锁允许多个goroutine同时读取共享资源,但当有goroutine需要写入资源时,会阻止其他goroutine的读和写。 这对于读多写少的场景可以提高并发性能。
package main
import (
"fmt"
"sync"
)
var (
counter int
rwMutex sync.RWMutex
)
func readCounter() {
rwMutex.RLock() // 读锁
fmt.Println("Counter:", counter)
rwMutex.RUnlock() // 释放读锁
}
func increment() {
rwMutex.Lock() // 写锁
counter++
rwMutex.Unlock() // 释放写锁
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
increment()
wg.Done()
}()
}
wg.Wait()
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
readCounter()
wg.Done()
}()
}
wg.Wait()
}
1.5 WaitGroup 计数器
代码示例
func ManyGowait() {
var wg sync.WaitGroup
wq.add(5)
for i:=0; i<5; i++ {
go func(j int){
defer wg.Done()
hello(j)
}(i)
}
wg.Wait()
}
2.依赖管理
2.1 介绍
概念
一个工程项目不可能仅仅依赖标准代码库0~1进行编码搭建,而是需要很多其他库,而管理这些库就是“依赖管理”。
演进
GoPath -> GoVendor -> GoModule
- 不同环境(项目)依赖的版本不同
- 控制依赖库的版本
2.2 GoPath
-
环境变量 $GoPath
-
项目代码直接依赖src下的代码
-
go get 下载最新版本的包到src目录下
存在的弊端
2.3 Go Vendor
- 项目目录下增加vendor文件,所有依赖包副本形式放在$ProjectRoot/vendor
- 依赖寻址方式:vendor=>GOPATH
通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题。
.
|__README.MD
|__dao
|__handler
|__main.go
|__service
|__vendor
存在的弊端
2.4 Go Module
- 通过 go.mod 文件管理依赖包版本
- 通过 go get/go mod 指令工具管理依赖包
依赖管理三要素
- 配置文件,描述依赖 go.mod
- 中心仓库管理依赖库 Proxy
- 本地工具 go get/mod
依赖配置 - go.mod
依赖配置 - version
- 语义化版本
${MAJOR}.$[MINOR).S[PATCH}V1.3.0 V2.3.0 - 基于commit伪版本 vX.0.0-yyyymmddhhmmss-abcdefgh1234 v0.0.0-20220401081311-c38fb59326b7 v1.0.0-20201130134442-10cb98267c6c
依赖配置 - indirect
A -> B -> C (红色框)
- A -> B 直接依赖
- A -> C 间接依赖
(黄色框)
- 主版本2+模块会在模块路径增加N后缀。
- 对于没有go.mod-文件并且主版本2+的依赖,会+incompatible
2.5 依赖分发



2.7 工具- go xxx


3.测试
3.1 单元测试

规则
-
所有测试文件以 ==_test.go== 结尾
-
func TestXxx(*testing.T)
-
初始化逻辑放到TestM中
例子
func HelloTom() string {
return "jerry"
}
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
if output != expectOutput {
t.Errorf("Expected %s do not match actual %s", expectOutput, output)
}
}
覆盖率
--用来衡量代码的测试水准
Tips
- 一般覆盖率:50%~60%,较高覆盖率80%+
- 测试分支相互独立、全面覆盖
- 测试单元粒度足够小,函数单元职责
3.2 Mock测试
依赖
Mock
打桩:用一个函数B(打桩函数)去替换函数A(原函数)
- 为一个函数打桩
- 为一个方法打桩

3.3 基准测试
& 随机选择服务器
package main
import (
"math/rand"
"testing"
)
var ServerIndex [10]int
func InitServerIndex() {
for i := 0; i < 10; i++ {
ServerIndex[i] = i + 100
}
}
func Select() int {
return ServerIndex[rand.Intn(10)]
}
func BenchmarkSelect(b *testing.B) {
InitServerIndex()
b.ResetTimer()
for i := 0; i < b.N; i++ {
Select()
}
}
func BenchmarkSelectParallel(b *testing.B) {
InitServerIndex()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Select()
}
})
}