GO语言进阶与依赖管理 | 青训营笔记

81 阅读4分钟

GO语言进阶与依赖管理

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

前言

本文记录了本人对Go语言的进阶部分和依赖管理的理解,当然也有自己没有掌握的部分。

一、本节课重点

    1. 语言进阶、并发编程
    • Goroutine
    • csp
    • channel
    • 并发安全 Lock
    • waitGroup
    1. 依赖管理
    • Go依赖管理演进
    • 依赖管理三要素
    • 依赖配置
    1. 单元测试
    • 单元测试
    • 单元测试-依赖
    • 单元测试-文件处理
    • 单元测试-Mock
    • 基准测试

二、详细介绍

语言进阶、并发编程

并发:多线程程序在一个核的cpu上运行

并行:多线程程序在多个核的cpu上运行

并行是实现并发的手段

为什么Gp语言快?

Go可以充分发挥多核优势、高效运行

1.1 Goroutine(协程)

  • 协程:用户态,轻量级线程,栈KB级别 创建、调度由GO本身完成
  • 线程:内核态,线程跑多个协程,栈MB级别 消耗资源,Go能一次创建上万个协程,所以Go语言能高并发
package main

import (
	"fmt"
	"time"
)

func hello(i int) {
	println("Hello goroutine :" + fmt.Sprint(i))  
        //可以写成fmt.println("Hello goroutine :",i)
}

func HelloGoroutine() {
	for i := 0; i < 5; i++ {
		go func(j int) {
			hello(j)
		}(i)
	}
	time.Sleep(time.Second) 
        //go用来指定睡眠时间的函数为time.Sleep()
        //time.Second是一个Duration类型,表示1s的时间间隔,乘系数3就是3s的时间间隔
}

func main() {
	HelloGoroutine()
}

屏幕截图 2023-01-17 155432.png

1.2 csp

屏幕截图 2023-01-17 160240.png

GO提倡通过通信共享内存而不是通过共享内存而实现通信

1.3 channel

make (chan 元素类型,[缓冲大小]) 根据是否有缓冲区大小而分为无缓冲通道和有缓冲通道

  • 无缓冲通道 make(chan int)
  • 有缓冲通道 make(chan int,2) 2表示缓冲大小为2

image.png

箭头代表通道方向,代表Gorountine1发送给Gorountine2

无缓冲通道:发送方与接收方同步化,也被称为同步通道

有缓冲通道:会发生阻塞

func CalSquare() {
	src := make(chan int)
	dest := make(chan int, 2)

	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 {
		println(i)
	}
}

func main() {
	CalSquare()
}

image.png

在Go语言中,使用defer关键字延迟一个函数或方法的执行,defer语句会在所有函数最后去执行

一开始使用defer close(),延迟执行close,关闭文件

当函数中出现多个defer语句,则在函数的最后逆序执行defer语句。(遵循栈的操作,先进后出)

func main() {
	defer fmt.Println("defer测试完毕")
	defer fmt.Println("world")
	defer fmt.Print("hello ")
	fmt.Println("测试defer")
}

image.png

如果defer语句遇到了参数传递。实际上,defer语句调用时参数就已经传递了,只是相关函数和语句会被放到函数的最后执行。后续语句对于参数的修改和先前的defer语句就没有关系了。

func main() {
	num := 10
	fmt.Println("定义num:", num)
	defer fmt.Println("defer参数传递 num:", num)

	num *= 2
	fmt.Println("计算后的num:", num)
}

image.png

1.4 并发安全 Lock

var (
	x    int64
	lock sync.Mutex
        //互斥锁,当goroutine获得Mutex后,其他goroutine只能等这个goroutine释放该Mutex
)

func addWithLock() {
	for i := 0; i < 2000; i++ {
		lock.Lock()
                //加锁
		x += 1
		lock.Unlock()
                //解锁
                //可以修改为 lock.LOck()  defer lock.Unlock()  x+=1
	}
}

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)
	fmt.Println("WithoutLock:", x)

	x = 0
	for i := 0; i < 5; i++ {
		go addWithLock()
	}
	time.Sleep(time.Second)
	fmt.Println("WithLock:", x)
}
func main() {
	Add()
}

image.png

1.5 WaitGroup 实现并发任务同步

计数器: 开启协程 +1;执行结束 -1;主协程阻塞直到计数器为0

package main

import (
	"fmt"
	"sync"
)

func hello(i int) {
	println("Hello goroutine :" + fmt.Sprint(i))
}

func ManyGoWait() {
	var wg sync.WaitGroup
	wg.Add(5)  //Add(delta int)  计数器 +delta
	for i := 0; i < 5; i++ {
		go func(j int) {
			defer wg.Done()  //Done() 计数器 -1
			hello(j)
		}(i)
	}
	wg.Wait()  //Wait() 阻塞直到计数器为0
}

func main() {
	ManyGoWait()
}

image.png

2. 依赖管理

单体函数,依赖原生的SDK

复杂项目,工程项目不可能基于标准库0~1编码搭建,关注业务逻辑,框架、集合等依赖SDk的引入 管理依赖库是很重要的

2.1 Go依赖管理演进

GOPATH -> GOVendor -> GOModule

不同环境(项目)依赖的版本不同

控制依赖库的版本

2.1.1 GOPATH

image.png

环境变量 $GOPATH

项目代码直接依赖src下的代码

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

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

2.1.2 GoVendor

项目目录下增加vendor文件,所有依赖包副本形式放在$ProjectRoot/vender

依赖寻址方式: vender-> GOPATH

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

弊端: 无法控制依赖的版本,更新项目可能出现依赖冲突,导致编译出错

2.1.3 GoMOdule

通过go.mod文件管理依赖包版本

通过go get/go mod指定工具管理依赖包

终极目标: 定义版本规则和管理项目依赖关系

2.2 依赖管理的三要素

  • 配置文件,描述依赖 go.mod
  • 中心仓库管理依赖库 proxy
  • 本地工具 go get/mod
2.3.1 依赖配置-go.mod
  • 依赖管理基本单元
  • 原生库
  • 单元依赖 屏幕截图 2023-01-17 104540.png
2.3.2 依赖配置-version
  • 语义化版本
  • 基于commit伪版本

屏幕截图 2023-01-17 104602.png

MAJOR{MAJOR} {MINOR} ${PATCH}

MAJOR 大版本,不同的MAJOR是不兼容的,代码隔离

MINOR 新增函数功能,在MINOR下保持兼容

PATCH 代码bug修复

vx.0.0 代表版本前缀 yyyymmddhhmmss代表时间戳 abcdefgh1234代表12位哈希码前缀

2.3.3 indirect

image.png 对于没有直接表示的模块会在go.mod 中加上//indirect

a->b->c a->b 表示直接依赖 a->c 表示间接依赖

incompatible

屏幕截图 2023-01-17 105110.png

2.3.4 依赖配置-依赖图

屏幕截图 2023-01-17 105317.png 若是有C1.5,也选择1.4版本,因为C1.4版本已经够编译了,不需要其他版本了,1。4版本是最低的兼容版本了

2.3.5 依赖分发-回源

image.png

无法保证构建稳定性 增加/修改/删除/软件版本

无法保证依赖可用性 删除软件

增加第三方压力 代码托管平台负载问题

依赖分发-Proxy

屏幕截图 2023-01-17 105446.png

2.3.6 依赖分发-变量GOPROXY

屏幕截图 2023-01-17 105537.png

2.3.7 工具-go get

屏幕截图 2023-01-17 105640.png

2.3.8 工具-go mod

屏幕截图 2023-01-17 105732.png

3.单元测试

image.png 测试是避免事故的最后一道屏障

屏幕截图 2023-01-17 110340.png

3.1单元测试

屏幕截图 2023-01-17 110457.png 单元测试能决定代码质量的保证,效率的提升

3.1.1 单元测试-规则
  • 所有测试文件以_test.go结尾

  • func TestXxx(*testing.T)

  • 初始化逻辑放到TestMain中

image.png

3.1.2 单元测试-assert
import (
	"testing"
	"github.com/stretchr/testify/assert"
)

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

func HelloTom() string {
	return "Tom"
}
3.1.3 单元测试-覆盖率
import "testing"

func JudgePassLine(score int16) bool {
	if score >= 60 {
		return true
	} else {
		return false
	}
}

func TestJudgePassLineTrue(t *testing.T) {
	isPass := JudgePassLine(70)
	assert.Equal(t, true, isPass)
}

若提高覆盖率,该加上这部分代码,覆盖率达到100%

func TestJudgePassLineFalse(t *testing.T) {
	isPass := JudgePassLine(50)
	assert.Equal(t, false, isPass)
}
  • 一般覆盖率:50%~60%,较高覆盖率:80%+,需要的成本提高

  • 测试分支相互独立,全面覆盖

  • 测试单元粒度足够小,函数单一职责

3.2 单元测试-依赖

屏幕截图 2023-01-17 115139.png

  • 幂等:重复运行同一个case,结果与之前一致
  • 稳定:指单元测试相互隔离,可以独立运行

3.3 端元测试-文件处理

func ReadFirstLine() string {
	open, err := os.Open("log")
	defer open.Close()
	if err != nil {
		return ""
	}
	scanner := bufio.NewScanner(open)
	for scanner.Scan() {
		return scanner.Text()
	}
	return ""
}

func ProcessFirstLine() string {
	line := ReadFirstLine()
	destLine := strings.ReplaceAll(line, "11", "00")
	return destLine
}

func TestProcessFirstLine(t *testing.T) {
	firstLine := ProcessFirstLine()
	assert.Equal(t, "line00", firstLine)
}

3.4 单元测试-Mock

快速Mock函数

  • 为一个函数打桩
  • 为一个方法打桩
func Patch(target, replacement interface{}) *PatchGuard {
	t := reflect.ValueOf(target)
	r := reflect.ValueOf(replacement)
	patchValue(t, r)
	return &PatchGuard{t, r}
}

func Unpatch(target interface{}) bool {
	return unpatchValue(reflect.ValueOf(target))
}

func TestProcessFirstLineWithMock(t *testing.T) {
	monkey.Patch(ReadFirstLine, func() string { 
        //对ReadFirstLine打桩测试,不在依赖本地文件
		return "line110"
	})
	defer monkey.Unpatch(ReadFirstLine)
	line := ProcessFirstLine()
	assert.Equal(t, "line000", line)
}

3.5 基准测试

  • 优化代码,需要对当前代码分析
  • 内置的测试框架提供了基准测试的能力
//随机选择执行服务器
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()
		}
	})
}

优化,提高性能近百倍

func FastSelect() int{
      return ServerIndex[fastrand.Intn(10)]
}

总结

本次课程本人学到了很多知识,知道了Go语言的并发编程,依赖管理,单元测试,进一步掌握Go语言的知识, 争取下次课程能再进一步。期待下节课的到来