go工程实践(一)| 青训营笔记

78 阅读5分钟

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

重点概览

  1. 从并发编程的角度了解高性能的本质
  2. 了解go语言依赖管理的演进路线
  3. 提升质量意识

详细介绍

go:为并发而生

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

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

并行:实现并发的一种手段

go实现了一个并发性能极高的调度模型,通过高效调度最大限度的利用计算资源,从而充分发挥多核计算机的优势

协程Goroutine

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

Go语言一次可以创建上万级别的协程。通过go关键字来开启goroutine;

goroutine是一个轻量级线程,其调度是由Golang运行时进行管理的

 //实现快速打印 
 func hello(i int) {
      println("hello gorountine: " + fmt.Sprint(i))
  }
  func HelloGoRoutine() {
      for i := 0; i < 5; i++ {
          go func(j int) {
              hello(j)
          }(i)//匿名函数
      }
      // 保证子协程完成前, 主线程不退出
      time.Sleep(time.Second)
  }

CSP(Communicating Sequential Processes)

通信顺序进程:

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

image-20230204203426090

Channel

make(chan 元素类型, [缓冲大小])

  • 无缓冲通道(同步通道) make(chan int)
  • 有缓冲通道 make(chan int, 2)

通道是用来传递数据的一个数据结构,可以用于两个goroutine之间,通过传递一个指定类型的值来同步运行和通讯。

 func CalSquare() {
      // src实现协程A和B的通信
      src := make(chan int)
      // dest实现协程B和主协程的通信
      // 这里用有缓冲通道, 是考虑到主协程复杂操作速度较慢,借缓冲协调速度不均衡
      dest := make(chan int, 3)
      go func() { // 协程A: 子协程发送0-9数字
          defer close(src)
          for i := 0; i < 10; i++ {
              src <- i
          }
      }()
  
      go func() { // 协程B: 计算输入数字的平方
          defer close(dest)
          for i := range src {
              dest <- i * i
          }
      }()
      // 主协程输入最后的平方数
      for i := range dest {
          // 复杂操作
          println(i)
      }
  }

并发安全Lock

Mutex互斥锁来实现同步

同步的含义:两个或多个协程之间互相等待,而不是顺次执行

  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 main() {
      x = 0
      for i := 0; i < 50; i++ {
          // 这是未定义行为, 应避免
          go addWithoutLock()
      }
      time.Sleep(time.Second)
      println("WithoutLock:", x)
      x = 0
      for i := 0; i < 50; i++ {
          go addWithLock()
      }
      time.Sleep(time.Second)
      println("WithLock:", x)
  }
WaitGroup实现同步:
 package main
 ​
 import (
     "fmt"
     "sync"
 )
 ​
 func HelloPrint(i int) {
     fmt.Println("Hello WaitGroup :", i)
 }
 ​
 func ManyGoWait() {
     var wg sync.WaitGroup
     wg.Add(5)
     for i := 0; i < 5; i++ {
         go func(j int) {
             defer wg.Done()
             HelloPrint(j)
         }(i)
     }
     wg.Wait()
 }
 ​
 func main() {
     ManyGoWait()
 }

依赖管理

依赖指各种开发包,我们在开发项目中,需要学会站在巨人的肩膀上,也就是利用已经封装好的、经过验证的开发组件或工具来提升自己的研发效率。

对于hello world以及类似的单体函数只需要依赖原生SDK,而实际工程会相对复杂,我们不可能基于标准库0~1编码搭建。

在实际开发中我们更多关注业务逻辑的实现,而其他的涉及框架、日志、driver、以及collection等一系列依赖都会通过sdk的方式引入,这样对依赖包的管理就显得尤为重要。

发展历程

GOPATH

是一个环境变量,其中有三个部分:

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

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

Go Vender

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

依赖寻址方式:vender -> GOPATH

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

存在的弊端:更新项目的时候可能导致编译错误的冲突;无法控制依赖的版本。

Go Module(1.16以后默认开启)
  • 通过go.mod文件管理依赖包版本
  • 通过go get/go mod指令工具管理依赖包

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

依赖管理三要素

  • 配置文件,描述以来——go.mod
  • 中心仓库管理依赖库——Proxy
  • 本地工具——go get/mod

测试

image-20230204211004377

单元测试

单元测试主要包括:输入,测试单元,输出,以及校对;

单元的概念比较广,包括接口,函数,模块等;用最后校对来保证代码的功能与我们的预期相符;

单元测试一方面可以保证质量,在整体覆盖率足够的情况下,一定程度上段保证了新功能本身的正确性,又未破坏原有代码的正确性。

另一方面可以提升效率,在代码有bug的情况下,通过编写单测,可以在一个较短周期内定位和修复问题。

单元测试规则
  • 所有测试文件以_test.go结尾(这样在使用go build进行构建时,测试代码才会被排除在外
  • 测试函数格式:func TestXxx(*testing.T)
  • 初始化逻辑放到TestMain中

image-20230206231231864

Mock测试

mock 测试就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法。这个虚拟的对象就是mock对象。mock对象就是真实对象在调试期间的代替品。

因为我们实际编写程序都不会是一个简单类,而是有着复杂依赖关系的类,Mock 对象让我们在不依赖具体对象的情况下完成测试。

monkey: github.com/bouk/monkey 这是一个开源的mock测试库

快速Mock函数:

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

基准测试

基准测试是指测试一段程序的性能及耗费CPU的程度;

在实际的项目开发中,经常会遇到代码性能瓶颈,为了定位问题,经常要对代码做性能分;

这时就用到了基准测试,其使用方法与单元测试类似。

  • 优化代码,需要对当前代码分析
  • 内置的测试框架提供了基准测试的能力