Go 语言进阶 | 青训营笔记

Go 工程进阶 | 青训营笔记

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

一、并发

  1. 概念

    (1)并发:多线程程序在一个核的 CPU 上运行 image.png (2)并行:多线程程序在多个核的 CPU 上运行 image.png

  2. 协程Goroutine

image.png 协程处于用户态,是轻量级的线程(一个线程可以跑多个协程),栈空间少(KB级别,线程是MB级别) 代码主干:开启多个线程

 func hello(i int) {
     println("hello goroutine : " + fmt.Sprint(i))
 }
 ​
 func HelloGoRoutine() {
     for i := 0; i < 5; i++ {
         go func(j int) {
             hello(j)
         }(i)
     }
     time.Sleep(time.Second)
 }

测试结果: image.png

  1. 通道Channel

image.png

(1)无缓冲通道:make(chan int)

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

代码主干:生成 0-9 之间的整数并计算他们的平方

 func CalSquare() {
     src := make(chan int)
     dest := make(chan int, 3)
     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)
     }
 }

代码测试: image.png

  1. 锁Lock

    代码主干:计数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)
     }
    

    代码测试: image.png

    (自己测试为什么都是10000)fo了

  2. 线程同步WaitGroup

    计数器开启协程 +1,执行结束 -1,主协程阻塞直到计数器为 0。 image.png

    代码主干:

     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.png

二、依赖管理

1. Go 依赖管理演进

1. GOPATH

image.png

  • GOPATH 是 Go 语言支持的一个环境变量,value 是 Go 项目的工作区。目录有以下结构:

    • src:存放 Go 项目的源码;
    • pkg:存放编译的中间产物,加快编译速度;
    • bin:存放 Go 项目编译生成的二进制文件。
  • 弊端:

    同一个 pkg,有 2 个版本 , A -> A(),B-> B(),而 src 下只能有一个版本存在,AB 项目无法保证都能泽通过。也就是在 gopath 管理模式下,如果多个项目依赖同一个库,则依赖该库是同一份代码,所以不同项目不能依赖同一个库的不同版本,这很显然不能足我们的项目依赖需求。为了解决这问题,govender 出现了。

2. Go Vendor

image.png

项目目录下增加 vendor 文件,所有依赖包副本形式放在 $ProjectRoot/vendor,在 vendor 机制下,如果当前项目存在 vendor 目录,会优先使库该目录下的依赖,如果依赖不存在,会从 GOPATH 中寻找。

弊端:

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

3. Go Module

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

2. 依赖管理三要素

1. 配置文件,描述依赖(go.mod)

image.png

indirect 后缀表示 go.mod 对应的当前模块,没有直接导入该依赖模块的包,也就是非直接依赖,标示间接依赖。

incompatible 后缀表示主版本 2+ 模块会在块路径增加 /vN 后缀,这能让 go module 按照不同的模块来处理同一个顶目不同主版本的依赖。由于 go module 是过往实验性引入,所以这顶规则提出之前已经有一些仓库打上了 2 或者更高版本的 tag 了,为了兼容这部分仓库,对于没有 go.mod 文件并且主版本在 2 或者以上的依赖,会在版本号后加上 +incompatible 后缀。

2. 中心仓库管理依赖库(proxy)

image.png

直接使用版管理仓库下载依赖,存在多个问题:

  • 无法保证构建确定性:软件作者可以直接在代码平台增加/修改/删除软件版本,导致下次构建使用另外版本的依赖,或者找不到依赖版本。

  • 无法保证依赖可用性:依赖软件作者可以直接在代码平台删除软件,导致依赖不可用:大幅增加第三方代码托管平台

    go proxy 就是解决这些问题的方案,Go Proxy 是一个服务站,它会缓存源站中的软件内容,缓存的软件版本不会改变,并且在源站软件删除之后依然可用,从而实现了供 "immutability" 和 "available" 的依赖分发,使用 Go Proxy 之后,构建时会直接从 Go Proxy 站点拉取依赖 。

3. 本地工具(go get / mod)

  1. go get image.png

  2. go mod image.png

三、单元测试

1. 单元测试概念和规则

1. 单元测试在开发流程所处的位置如图:

image.png

2. 规则:

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

  • 测试函数的名称为 func TestXxx(*testing.T) image.png

  • 初始化逻辑放到 TestMain 中 image.png

2. Mock测试

image.png 快速 Mock 函数:

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

对 ReadFirstLine 打桩测试,不再依赖本地文件 image.png

3. 基准测试

1. 例子

image.png

2. 运行

image.png

3. 优化

image.png


介绍了 Go 一些和工程实践密切相关的介绍和操作,包括并发,依赖和测试。

在工程项目的进行中,并发需要事先构建事件在时间上的运行逻辑,然后根据这些逻辑安排代码的结构,避免一些代码的冗余;依赖经过多种方式的演变,在可用性上有着更加方便的体验;测试是一个比较繁琐的过程,有时会耗费一半以上的项目时间进行测试,从而保证项目指标达到预期。