Go语言依赖与测试|青训营笔记

78 阅读4分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第2篇笔记

1.语言进阶

1.1并发VS并行

  • 并发:多线程程序在一个核的CPU上运行
  • 并行:多线程程序在多个核的CPU上运行
  • go语言可以充分发挥多核的优势,为并发而生

1.2协程VS线程

  • 协程:用户态,轻量级线程,栈KB级别
  • 线程:内核态,线程跑多个协程,栈MB级别

1.3 CSP(Communicating Sequential Processes)

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

1.4 Channel

  • 这里主要是介绍了Channel,有缓冲、无缓冲、引用类型这些

1.5 Lock

  • 展示了Lock和WaitGroup在并发的情况下起到的作用
package concurrence

import (
  "sync"
  "time"
)

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 < 10; i++ {
    go addWithoutLock()
  }
  time.Sleep(time.Second)
  println("WithoutLock:", x)
  x = 0
  for i := 0; i < 10; i++ {
    go addWithLock()
  }
  time.Sleep(time.Second)
  println("WithLock:", x)
}

func ManyGoWait() {
  var wg sync.WaitGroup
  wg.Add(5)
  for i := 0; i < 5; i++ {
    go func(j int) {
      defer wg.Done()
      hello(j)
    }(i)
  }
  wg.Wait()
}

image_9cM7ZHpxTK.png

image_m1tnkyzDxy.png

2.依赖管理

2.1 GOPATH、Go Vendor、Go Module简介

  • 不同环境(项目)依赖的版本不同
  • 控制依赖库的版本
  1. GOPATH,环境变量,GO项目的一个工作区

    1. 结构

      1. bin 项目编译的二进制文件

      2. pkg 项目编译的中间产物,加速编译

      3. src 项目源码

        1. 项目代码直接依赖src下的代码
        2. go get下载最新版本的包到src目录下
    2. 弊端

      1. 无法实现package的多版本控制

        image_xEFHd5todM.png

  2. Go Vendor

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

      image_9gcyukJVY_.png

    2. 现在vendor下寻找依赖,再向GOPATH寻找

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

    4. 弊端

      image_8mbEmHNpRb.png

      • 无法控制依赖的版本
      • 更新项目有可能出现依赖冲突,导致编译出错
  3. Go Mudule

    1. 通过go.mod文件管理依赖包版本
    2. 通过go get/go mod指令工具管理依赖包
  4. 依赖管理三要素

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

2.2 示例分析

  1. go mod

    image_shaLLWAW-L.png

  2. version

    1. 语义化版本 MAJOR.{MAJOR}.{MINOR}.${PATCH} 例如 V1.3.0
    2. 局域commit伪版本 vX.0.0-yyyymmddhhmmss-abcdefg1234
  3. 关键字

    1. indirect

      image_nR6BTwZO0f.png

    2. incompatible

      image_ITDx1UW3Kz.png

      image_CliWyRDsuh.png

2.3 依赖分发

GOPROXY="proxy1.cn,https://proxy2.cn,…"

服务站点URL列表,“direct”表示源站,如果前面的代理都找不到的话,那么将会“回源”

2.4 工具

  1. go get

    image_4g59kt5ygv.png

  2. go mod

    image_FdW3XgkmgY.png

3.测试

3.1 单元测试

  1. 组成:输入、测试单元、输出、与期望校对

  2. 作用:保证质量,提升效率

  3. 规则:

    • 所有测试文件以_test.go结尾
    • 函数func TestXxx(*testing.T)
    • 初始化逻辑放到func TestMain (m *testing.M)中
  4. 可以用一些外部的assert库来判断结果

    image_Ry3aZLgDJu.png

  5. 单元测试水准的标准——覆盖率

3.2 Mock测试

  • 一个常见的Mock测试包,monkey :github.com/bouk/monkey

  • 为一个函数打桩

    被测试函数

    package test
    import (
      "bufio"
      "os"
      "strings"
    )
    
    func ReadFirstLine() string {
      open, err := os.Open("log")
      /*
        log中内容为
        line11
        line22
        line33
      */
      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
    }
    

    测试函数

    package test
    
    import (
      "bou.ke/monkey"
      "github.com/stretchr/testify/assert"
      "testing"
    )
    
    func TestProcessFirstLine(t *testing.T) {
      firstLine := ProcessFirstLine()
      assert.Equal(t, "line00", firstLine)
    }
    //这两个函数的结果都是PASS,但是不同的是,上面的那个函数需要依赖于log文件,而下面这个打桩函数不需要依赖于本地文件,用一个新的返回“line11”的函数代替
    func TestProcessFirstLineWithMock(t *testing.T) {
      //对ReadFirstLine函数做一个打桩操作,不依赖本地文件
      monkey.Patch(ReadFirstLine, func() string {
        return "line110"
      })
      //打桩函数的卸载
      defer monkey.Unpatch(ReadFirstLine)
      line := ProcessFirstLine()
      assert.Equal(t, "line000", line)
    }
    
  • 为一个方法打桩

3.3 基准测试

  1. 例子:随机选择执行服务器

    image_VFXI9wqPq_.png

  2. 运行

    1. 测试函数以Benchmark开头

      package benchmark
      
      import (
        "github.com/bytedance/gopkg/lang/fastrand"
        "math/rand"
      )
      
      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)]
      }
      
      package benchmark
      
      import (
        "testing"
      )
      
      //BenchmarkSelect-8  70160442  16.90 ns/op
      func BenchmarkSelect(b *testing.B) {
        InitServerIndex()
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
          Select()
        }
      }
      
      //BenchmarkSelectParallel-8  22544683  1.77 ns/op
      func BenchmarkSelectParallel(b *testing.B) {
        InitServerIndex()
        b.ResetTimer()
        b.RunParallel(func(pb *testing.PB) {
          for pb.Next() {
            Select()
          }
        })
      }
      
      //BenchmarkFastSelectParallel-8   1000000000  1.102 ns/op
      func BenchmarkFastSelectParallel(b *testing.B) {
        InitServerIndex()
        b.ResetTimer()
        b.RunParallel(func(pb *testing.PB) {
          for pb.Next() {
            FastSelect()
          }
        })
      }
      

    测试可以得知fastrand.Intn()的速度更快,但同时也牺牲了一些随机数量的一致性,rand再高并发场景会有性能问题

4. 提问

  1. 协程和用户线程的区别?

    答:协程可以理解为用户态的线程,但是为了和内核态的线程做个区分。

  2. 项目中实现鉴权什么时候用session+cookie的方式,什么时候用jwt token的方式呢?

    答:一般和端上是session+cookie,http和服务端交互的时候一般用jwt。

  3. go可以充分利用多核的优势,而其他语言类似java,无法充分利用

    答:因为go是自己实现了一个用户态线程的调度,而其他语言更多是依赖系统的调度。