Go语言进阶和路径管理 | 青训营

42 阅读4分钟

Go语言进阶与路径管理

2.1 语言进阶(从并发编程的视角了解Go高性能的本质)

  • 01 并发与并行: 并发(多线程程序在一个核CPU上运行)、 并行(多线程程序在多个核的CPU上运行)。并行可以理解成实现并发的一个手段,Go语言可以充分发挥多核优势,高效运行。
    • 1.1 Goroutine(协程),线程是系统中昂贵的资源,属于内核态,其创建、切换、停止都属于重的操作,栈MB级别。协程可以理解为轻量级的线程,属于用户态,栈KB级别。(开启协程的方法,在调用函数的时候在函数前面加上go关键字即可)
      // 快速打印hello goroutine:0~hello goroutine 4
      func hello(i int){
          println("hello goroutine :"+fmt.Sprint(i))
      }
      
      func HelloGoRoutine(){
          for i := 0; i<5; i++{
              go func(j int){
                  hello(j)
              }
          }
          time.Sleep(time.Second) // 阻塞,为了保证协程结束前线程不退出
      }
      
    • 1.2 GSP(Comunicating Sequential Processes):协程之间的通信,Go提倡通过通信共享内存,而不是通过共享内存实现通信。前者:协程之间有通道连接。后者:通过共享内存
    • 1.3 Channel 通过make关键字创建,make(chan 元素类型, [缓冲大小])
      • 无缓冲通道(两个GoRoutine之间无缓冲,发送接收同步) make(chan int)
      • 有缓冲通道(会阻塞发送) make(chan int,2)
      // A子协程发送0~9数字
      // B子协程计算输入数字的平方
      // 子协程输出最后的平方数
      func CalSquare(){
          scr := make(chan int)       // 创建无缓冲chan
          dest := make(chan int,3)    // 创建有缓冲通道
          // 通过src这个chan实现A协程和B协程之间的通信
          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{
              fmt.Println(i)
          }
      }
      
    • 1.4 并发安全 Lock
      // 对变量执行2000次+1操作,5个协程并发执行
      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)
          fmt.Println("WithoutLock:",x)   //8382
          x = 0
          for i := 0; i <5; i++{
              go addWithLock()
          }
          time.Sleep(time.Second)
          fmt.Println("WithLock:",x)      // 10000
      }
      
    • 1.5 WaitGroup, 前面使用time.Sleep实现阻塞不够优雅,Go中常用WaitGroup实现并发任务的同步,在sync包下面。三个方法分别是Add(delta int):计数器+delta、Done():计数器-1、Wait():阻塞直到计数器为0.实际上WaitGroup内部维护了一个计数器,开启协程计数器+1,执行结束-1;主协程阻塞直到计数器为0.
      // 快速打印hello goroutine:0~hello goroutine 4
      func hello(i int){
          println("hello goroutine :"+fmt.Sprint(i))
      }
      
      func ManyGoWait(){
          var wg sync.WaitGroup
          wg.Add(delta:5)
          for i := 0; i<5; i++{
              go func(j int){
                  defer wg.Done()
                  hello(j)
              }
          }
          wg.Wait()
      }
      

2.2 依赖管理(了解Go语言依赖管理的演进路线)

  • 2.1 Go依赖管理演进:GOPATH----Go Vendor----Go Module,现在广泛使用的是Go Module。围绕以下两点

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

    • 控制依赖库的版本

    • GOPATH:是Go项目的工作区,bin:项目编译的二进制文件,pkg:项目编译的中间产物,加速编译,src:项目源码;项目代码直接依赖src下的代码; go get下载最新版本的包到src目录之下

      • GOPATH的弊端:假设项目A和项目B依赖于某个包的不同版本,无法实现package的多版本控制
    • Go Vendor:

      • 在项目目录下增加了verdor文件,所有依赖包副本形式放在$ProjectRoot/vendor
      • 依赖寻址方式:vendor => GOPATH
      • 通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题
      • Go Vendor弊端:假设项目A依赖于package B和package C,B和C依赖于package D的两个版本,就会存在无法控制依赖的版本、更新项目出现依赖冲突,导致编译出错两个问题。
    • Go Module

      • 通过go.mod文件管理依赖包版本
      • 通过go get/go mod 指令工具管理依赖包
      • 终极目标:定义版本规则和管理项目依赖关系
  • 2.2 依赖管理的3要素

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

    • 2.3.1 依赖配置-go.mod
      module github.com/wangkechun/go-by-example      //依赖管理基本单元
      go 1.18             //原生库
      require(        //单元依赖
          ~
      )
      
    • 2.3.2 依赖配置-version
      • 语义化版本 ${MAJOR}.${MINOR}.${PATCH},eg: V1.3.0
      • 基于commit伪版本vX.0.0-yyyymmddhhmmss-abcdefgh1234
    • 2.3.3 依赖配置-indirect,A->B->C,A直接依赖B,间接依赖C,间接依赖会用indirect标识
    • 2.3.4 依赖配置-imcompatible,
      • 主版本2+模块会在模块路径增加/vN后缀
      • 对于没有go.mod文件并且主版本2+的依赖,会+imcompatible标识
    • 2.3.5 依赖分发-Proxy
      • 保证了依赖的稳定性和可靠性
    • 2.3.6 工具-go getgo get example.org/pkg
      • @updata 默认
      • @none 删除依赖
      • @v1.1.2 tag版本,语义版本
      • @23dfdd5 特定的commit
      • @master 分支的最新commit
    • 2.3.7 工具go mod
      • init 初始化,创建go,mod文件

      • download 下载模块到本地缓存

      • tidy 增加需要的依赖,删除不需要的依赖

2.3 测试(从单元测试实践出发、提升质量意识)

  • 测试分为回归测试、集成测试、单元测试
    • 回归测试:模拟用户使用
    • 集成测试:以功能为主
    • 单元测试
  • 3.1 单元测试(输入————测试单元(函数、模块……)————输出————校对):保证质量、提升效率。
    • 3.1.1 单元测试规则:
      • 所有测试文件都以_test.go结尾
      • 测试函数命名规范:func TestXxx(*testing.T)
      • 初始化逻辑放到TestMain中,例如在测试前进行数据装载、配置初始化等前置工作,在测试后进行释放资源等收尾工作。
    • 3.1.2 使用go test xxx_test.go xxx.go指令进行测试
    • 3.1.3 可以使用开源assert包来实现对实际输出和期望输出的比较
    • 3.1.4 单元测试————覆盖率(用来评估单元测试水准)使用go test xxx_test.go xxx.go --cover在单测的同时得到覆盖率,覆盖率就是指测试了代码的占比
    • 3.3.5 单元测试————Tips
      • 一般覆盖率:50%~60%,较高覆盖率80%+
      • 测试分支相互独立、全面覆盖
      • 测试单元粒度足够小,函数单一职责
  • 3.2 测试单元————依赖(单元依赖于File、DB、Cache,要求实现测试是幂等、稳定的)
  • 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")		//将“11”替换为“00”
        return destLine
    }
    
    func TestProcessFirstLine(t *testing.T) {
    firstLine := ProcessFirstLine()
    assert.Equal(t, "line00", firstLine)		//这个测试是依赖于文件的,如果文件被篡改或者删除,那么这个测试将无法运行
    }
    
  • 3.4 单元测试————Mock(打桩)
    • 打桩可以理解为用一个函数A去替换一个函数B,B就是原函数,A是打桩函数
    • 对ReadFirstLine进行打桩测试,不再依赖本地文件
    func TestProcessFirstLineWithMock(t *testing.T) {
        monkey.Patch(ReadFirstLine, func() string {         // 打桩操作
            return "line110"
        })
        defer monkey.Unpatch(ReadFirstLine)                 // 在测试结束后卸载桩
        line := ProcessFirstLine()
        assert.Equal(t, "line000", line)
    }
    
  • 3.5 基准测试
    • 优化代码,需要对当前代码进行分析
    • 内置的测试框架提供了基准测试能力
    • 基准测试函数以Benchmark开头,入参是*testing.B
    • 使用go test -bench .指令执行所有的基准测试文件
        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 FastSelect() int {
            return ServerIndex[fastrand.Intn(10)]
        }
    
        func BenchmarkSelect(b *testing.B) {
            InitServerIndex()			// init服务器列表
            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 BenchmarkFastSelectParallel(b *testing.B) {
            InitServerIndex()
            b.ResetTimer()
            b.RunParallel(func(pb *testing.PB) {
                for pb.Next() {
                    FastSelect()
                }
            })
        }
    
    
    输出为:
        goos: windows
        goarch: amd64
        pkg: github.com/Moonlight-Zhao/go-project-example/benchmark
        cpu: AMD Ryzen 7 5800H with Radeon Graphics
        BenchmarkSelect-16                      156641480                7.605 ns/op
        BenchmarkSelectParallel-16              33325927                34.97 ns/op
        BenchmarkFastSelectParallel-16          1000000000               0.4999 ns/op
        PASS
        ok      github.com/Moonlight-Zhao/go-project-example/benchmark  3.936s
    
    • 基准测试————优化(前面并行性能劣化的原因是rand锁,用fastrand)