(二)Go语言入门 - 工程实践 | 青训营笔记

100 阅读3分钟

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

1.语言进阶

1.1 并发 VS 并行

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

image-20220508101046037.png

image-20220508101107960.png

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

1.2 Goroutine

协程:用户态,轻量级线程,栈MB级别

线程:内核态,线程跑多个协程,栈KB级别

image-20220508101537677.png

1.3 CSP(Communicating Sequential Process)

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

1.4 Channel

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

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

image-20220508103406213.png

区别:类似于生产与消费,无缓冲通道边生产边消费,有可能出现断货情况;而有缓冲通道把生产的产品囤积一部分,能尽量避免供不应求的情况。

1.5 并发安全 Lock

在多线程并发中可能会出现信息流失,导致安全问题,为避免这种情况,需对信息进行加锁操作

例:对变量执行2000次+1操作,5个协程并发执行,预期结果为10000

 //定义变量
 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)
 }
 ​
 /*
 WithoutLock: 8576
 WithLock: 10000
 */

可以看到不加锁的输出结果不一定为预期结果,说明不加锁存在信息流失

1.6 WaitGroup

计数器:开启协程+delta(Add(delta) );执行结束-1(Done() );主协程阻塞直到计数器为0(Wait() )

 func hello(i int) {
     println("hello goroutine : " + fmt.Sprint(i))
 }
 ​
 func HelloGoRoutine() {
     var wg sync.WaitGroup
     wg.Add(5)                   //delta=5
     for i := 0; i < 5; i++ {
         go func(j int) {
             defer wg.Done()     //delta-1
             hello(j)
         }(i)
     }
     wg.Wait()                   //delta=0终止
 }
 ​
 /*
 hello goroutine : 4
 hello goroutine : 2
 hello goroutine : 3
 hello goroutine : 1
 hello goroutine : 0
 */

2.依赖管理

2.1 背景

Go依赖管理演进:GOPATH -> GO Vender -> Go Module

GoModule:

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

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

2.2 依赖管理三要素

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

2.3 依赖配置

go.mod

依赖标识:[Module Path] [Version/Pseudo-version]

version

  • 语义化版本:​​​

    • V1.3.0;V2.3.0
  • 基于commit伪版本:vX.0.0-yyyymmddhhmmss-abcdefgh1234

    • v0.0.0-20220401081311-c38fb59326b7

indirect

间接依赖:A->B->C,A到B是直接依赖,A到C是间接依赖

incompatible

  • 主版本2+模块会在模块路径增加/vN后缀
  • 对于没有go.mod文件并且主版本2+的依赖,会+incompatible

2.4 依赖分发

回源

image-20220508114456817.png

存在问题:

  • 无法保证构建稳健性(增加/修改/删除软件版本)
  • 无法保证依赖可用性(删除软件)
  • 增加第三方压力(代码托管平台负载问题)

Proxy

image-20220508114639717.png

变量 GOPROXY

Proxy 1 --> Proxy 2 --> Direct(源站)

前面站点都没有依赖的话会回源到第三方代码平台上

go get

go get example.org/pkg

  • @update 默认
  • @none 删除依赖
  • @v1.1.2 tag版本,语义版本
  • @23dfdd5 特定的commit
  • @master 分支的最新commit

go mod

  • init 初始化,创建go.mod文件
  • download 下载模块到本地缓存
  • tidy 增加需要的依赖,删除不需要的依赖

3.测试

回归测试 --> 集成测试 --> 单元测试 (覆盖率逐层变大,成本逐层降低)

3.1 单元测试

通过单元测试得到的输出值与期望值进行校对。可以保证质量、提升效率

规则

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

  • func TestXxx(*testing.T)

     func TestPublishPost(t *testing.T){}
    
  • 初始化逻辑放到TestMain中

     func TestMain(m *testing.M) {
         
         //测试前:数据装载、配置初始化等前置工作
         
         code := m.Run()
         
         //测试后:释放资源等收尾工作
         
         os.Exit(code)
     }
    

覆盖率

编写的代码中被成功执行的代码所占的比例,可以用来判断代码编写的一系列功能是否都能成功运行,是否存在有些功能不能运行而产生bug等。

  • 一般覆盖率:50%-60%,较高覆盖率%80+
  • 测试分支相互独立、全面覆盖
  • 测试单元粒度足够小,函数单一职责

3.2 依赖

image-20220508154346848.png

外部依赖 => 稳定&幂等

3.3 Mock

monkey:github.com/bouk/monkey

快速Mock函数:

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

3.4 基准测试

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