Go之旅Day2 | 青训营

99 阅读3分钟

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)
}
image-20230726145647864

1.2 CSP

在Go语言中,CSP代表通信顺序进程(Communicating Sequential Processes)。CSP是一种并发编程模型,用于在并发程序中管理不同进程(goroutines)之间的通信和同步。它由计算机科学家Tony Hoare于1978年首次提出,旨在帮助程序员设计并发系统,并提供简单可靠的方法来处理并发问题。

核心思想是通过消息传递来实现不同的goroutines之间的通信,而不是通过共享内存。每个goroutine都是一个独立的执行单元,通过通道(channel)来发送和接收消息。通道是用于在不同goroutines之间传递数据的管道。

CSP的基本概念包括:

  1. Goroutines(进程):Go语言中的轻量级线程,允许同时执行多个函数,从而实现并发。
  2. 通道(Channel):通道是用来在goroutines之间传递数据的管道。发送和接收操作是阻塞的,这意味着在发送和接收数据之前,goroutine会等待通道的操作完成。
  3. 发送(Send):将数据从一个goroutine发送到通道。
  4. 接收(Receive):从通道接收数据,并将其从通道中删除。
  5. 同步(Synchronization):通过通道来同步不同的goroutines,确保它们按预期顺序执行。
image-20230726150157065

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.Mutexsync.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 计数器

image-20230726152018524

代码示例

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

    image-20230726153911315
  • 项目代码直接依赖src下的代码

  • go get 下载最新版本的包到src目录下

存在的弊端

image-20230726154200494

2.3 Go Vendor

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

通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题。

.
|__README.MD
|__dao
|__handler
|__main.go
|__service
|__vendor

存在的弊端

image-20230726155049791

2.4 Go Module

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

依赖管理三要素

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

依赖配置 - go.mod

image-20230726155519150

依赖配置 - 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 依赖分发

image-20230726161210274

image-20230726161239418

image-20230726161342521

2.7 工具- go xxx

image-20230726161427810

image-20230726161502147

3.测试

3.1 单元测试

image-20230726164201601

规则

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

    image-20230726165829150
  • func TestXxx(*testing.T)

    image-20230726165854572
  • 初始化逻辑放到TestM中

    image-20230726165909291

例子

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测试

依赖

image-20230726172555290

Mock

打桩:用一个函数B(打桩函数)去替换函数A(原函数)

  • 为一个函数打桩
  • 为一个方法打桩

image-20230726173144878

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()
		}
	})
}