go语言进阶--工程进阶|青训营笔记

106 阅读13分钟

这是我参与【第五届青训营】伴学笔记创作活动的第二天。

一、概述

本节课程主要分为四个方面:

1.并发编程(语言进阶)

从并发编程的视角待大家了解Go高性能的本质

并发

  • 多线程程序在一个核的cpu上运行,就是并发。

图片.png

并行

  • 多线程程序在多个核的cpu上运行,就是并行。

图片.png

注:并发不是并行,并发主要由切换时间片来实现”同时”运行,并行则是直接利用多核实现多线程的运行。 而GO可以充分发挥多核优势,高效运行。

协程Goroutine

在一个go程序里面,main函数对应了主线程,而在主线程里面开一个轻量级的线程就被叫做线程。

图片.png

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

使用go关键字即可快速开启一个协程,下面为教程中的一个例子。

图片.png

通道Channel

协程之间用来通信和传输数据的工具叫通道,分为有缓存通道和无缓冲通道。顾名思义,缓冲通道可以提供一片空间暂时存放数据,不过当缓冲满后需要释放缓冲才可继续传输数据。通道的简单定义如下:

//make(chan 元素类型,缓冲大小)
make(chan int,2)//有缓冲通道 
make(chan int)//无缓冲通道

举一个教程中的例子:

图片.png

图片.png

锁Lock

并发编程必然涉及到一个并发安全的问题,多个不同的线程可能会对同一块内存空间进行操作造成内存泄漏等很多安全问题,这时候就需要一个锁来保证内存空间的安全。

var lock sync.Mutex//定义一个锁
lock.Lock()//上锁
lock.UnLock()//解锁

我们来看一个教程中的例子:

图片.png 我们可以看到加锁和不加锁得到的结果是不一样的,对x那个变量加锁后保证了安全,使得计算结果准确。

线程同步WaitGroup

属于编程进阶内容,考虑到工程项目地可用性和可靠性,工程实战中经常会用到。在Goroutine那里额样例里面协程的阻塞是使用延时来进行的,但在实际情况中我们并不能具体的知道准确且合理的时间,这时候就要引入WaitGroup来解决问题。

//定义一个WaitGroup
var wg sync.WaitGroup
wg.Add(5)//计数器+5
wg.Done()//计数器-1
wg.Wait()//阻塞直到计数器为0

来看一下WaitGroup的使用:

图片.png

2.依赖管理

目标:了解Go语言依赖管理的演进的路线。 Go依赖管理经历了3个阶段:

  • 早期GOPATH
  • 中期Go Vendor
  • 最新Go Module

目前被广泛应用的是 Go Module,整个演进路线主要围绕实现两个目标来迭代发展:

  • 在不同环境 (项目) 依赖的库的版本不同
  • 控制并管理依赖库的版本
旧版本依赖管理--早期Gopath

GOPATH 是一个环境变量,用来表明你写的 go 项目的存放路径,GOPATH 路径最好只设置一个,所有的项目代码都放到 GOPATH 的 src 目录下。

windows下环境配置内容:

  • 系统变量GOPATH:D:\CODEFile01\GoPro
  • path添加路径:go编译器路径 和 GOPATH对应文件夹路径

图片.png 在GOPATH目录下新建三个文件:

  • bin:用来存放编译后生成的可执行文件
  • pkg:用来存放编译后生成的归档文件
  • src:用来存放源码文件

-------缺点:

图片.png

  • 项目A 和项B 依赖于某一 package 的不同版本 (分别为 Package V1Package V2 ) 。而 src 下只能允许一个版本存在,那项目A 和项B 就无法保证都能编译通过。
  • 在 GOPATH 管理模式下,如果多个项目依赖同一个库,则依赖该库是同一份代码,无法做到不同项目依赖同一个库的不同版本。
过渡版本依赖管理——中期Go Vendor

面对早期GOPATH的依赖管理方式存在的弊端出现了Go Vendor的解决方案。 中期Go Vendor依赖管理的项目结构:

  • 环境变量GOPATH指定文件夹路径
  • 手动创建 bin/ 、pkg/ 、src/ 三个文件夹
  • 新增vendor文件

-vendor文件其实是一个目录,所在的该项目使用依赖包会以副本形式放在vendor目录下。这个时候,导入包会引入优先级:

  • 当前项目存在 Vendor 目录,会优先使用该目录下的依赖
  • 如果依赖不存在,则会从 GOPATH 中寻找

图片.png

看到这里,因该会明白了与早期GOPATH管理方式的不同之处。

-------缺点:

图片.png

  • Vendor 无法很好解决依赖包版本变动问题和一个项目依赖同一个包的不同版本的问题。
  • 项目A依赖项目B与项目C,项目B依赖项目D-V1,项目C依赖项目D-V2,这样底层同一个项目A底层依赖了同一个项目D的不同版本,一旦更新,又该如何指定不同的依赖版本则成了问题。
新版本依赖管理——最新Go Module

目标--了解Go依赖管理演进地历程,通过课程学习以及课后实践能够熟练使用go module 管理依赖。 Go Module 自 Go1.11 开始引入,Go 1.16 默认开启。可以在项目目录下看到 go.mod 文件。

图片.png Go Module实现依赖管理三个要素:

图片.png

  • 配置文件go.mod用于描述依赖
  • 代理Proxy实现中心仓库管理依赖库
  • go mod 操作命令用于管理依赖库初始化、更新
依赖配置
依赖配置 - go.mod

图片.png 模块路径用来标识一个模块,从模块路径可以看出从哪里可以找到该模块,如果是 github 前缀则表示可以从 Github 仓库中找到该模块,依赖包的源代码由 github 托管,如果项目的子包想被单独引用,则需要通过单独的 init go.mod 文件进行管理。

中间的则是依赖的原生库 sdk 版本。

最下面的是单元依赖,每个依赖单元由模块路径+版本来唯一标识。

依赖配置 - version

图片.png

gopath 和 govendor 都是源码副本方式依赖,没有版本规则的概念,而 gomod 为了方便管理则定义了版本规则,分为语义化版本和基于 commit 伪版本;其中 语义化版本包括 ${MAJOR}.${MINOR}.${patch},不同的 MAJOR 版本标识不兼容的API,所以即使是同一个库,MAJOR版本不同也会被认为是不同的模块;MINOR 版本通常是新增函数或功能,向后兼容;而 patch 版本一般是修复bug; 而基于commit的版本包括 vx.0.0-yyyymmddhhmmss-abcdefgh1234,基础版本前缀是和语义化版本一样的;时间戳(yyyymmddhhmmss),也就是提交 Commit 的时间,最后是校验码(abcdef),包含12位的哈希前缀;每次提交 commit 后 GO 都会默认生成一个伪版本号。

依赖配置 - indirect

图片.png

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

依赖配置 - incompatible

图片.png

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

之前讲语义化版本中,对于同一个库的不同 major 版本,需要不同的pkg目录,用不同的gomod文件管理。例如:V1版本在gomod在主目录下,而对于V2版本,则单独有V2目录,用另一个gomod文件管理依赖路径,来表名不同 major 的不兼容性。对于有些 V2+tag 版本的依赖包并未遵循这一定义规则,就会打上 incompatible 标志。

依赖配置 - 依赖图

图片.png

依赖分发 - 回源

图片.png go 的依赖分发就是从哪里下载,如果下载的问题

github 是比较常见的代码托管系统平台,而 go modules 系统中定义的依赖,可以对应到多版本代码管理系统中某一项目的特定提交或版本。此时对于gomod中定义的依赖,可以直接从对应仓库中下载指定软件依赖,从而完成依赖分发。

我们发现,直接使用版本管理仓库下载依赖,存在一些问题。

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

2.依赖软件作者可以直接代码平台删除软件,导致依赖不可用;

3.大幅增加第三方代码托管平台压力。

依赖分发 - Proxy

图片.png go proxy 可以解决上面提出的这些问题。go proxy 是一个服务站点,它会缓冲源站中的软件内容,缓存的软件版本不会改变,并且在源站软件删除之后依然可用,从而实现了供“immutablity”和“available”的依赖分发;使用 go proxy 之后,构建时会直接从 go proxy 站点拉取依赖,

依赖分发 - 变量 GOPROXY

图片.png Go Modules 通过 GOPROXY 环境变量控制如何使用 GO PROXY;

GOPROXY是一个 GO PROXY 站点URL列表,可以使用 direct 表示源站。

对于示例配置,整体的依赖寻址路径,会优先从 proxy1下载依赖,如果 proxy1 不存在,就去 proxy2 寻找,如果 proxy2 中不存在则会回源到源站直接下载依赖,缓存到 proxy 站点中。

工具 - go get

这个是go module 的管理工具下使用的。

图片.png

工具 - go mod

图片.png 我们要注意,在提交之前执行下 go tidy,减少构建时无效依赖包的拉取。

3.测试

在实际工程开发中,另一个重要概念就是单元测试,这里我们主要讲解go测试相关的内容,包括单元测试 、Mock测试以及基准测试

从单元测试实践出发,提升大家的质量意识。

事故

图片.png

  1. 营销配置错误,导致非预期用户享受权益,资金损失10w+
  2. 用户提现,幂等失效,短时间可以多次提现,资金损失20w+
  3. 代码逻辑错误,广告位被占,无法出广告,收入损失500w+
  4. 代码指针使用错误,导致APP不可用,损失上kw+。

图片.png

测试是避免事故的最后一道屏障

图片.png

测试一般分为, 回归测试一般是QA同学 手动通过终端回归一些固定的主流程场景,集成测试是对系统功能维度做测试验证,而单元测试测试开发阶段,开发者对单独的函数、模块做功能验证,层级从上至下,测试成本逐渐减低,而测试覆盖率确逐步上升,所以单元测试的覆盖率-定程度上决定这代码的质量。

1)单元测试概念和规则 2)Mock测试 3)基准测试 测试决定质量,质量决定生命 测试:回归测试,集成测试,单元测试,从左到右测试覆盖率变大,成本变小

图片.png 单元测试主要包括,输入,测试单元,输出,以及校对,单元的概念比较广,包括接口,函数,模块等;用最后的校对来保证代码的功能与我们的预期相符;

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

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

单元测试-规则

从文件上就很好了区分源码和测试代码,以Test开头, 且连接的第一个字母大写。单元测试规则:所有测试文件以_text.go结尾;func TextXxx(*testing.T);初始化逻辑放到TexrMain中 测试评判标准:代码覆盖率。

图片.png

单元测试例子

图片.png

单元测试-运行

图片.png

单元测试- assert

图片.png

单元测试-覆盖率

图片.png

图片.png

图片.png

  • 一般覆盖率: 50%~60%,较高覆盖率80%+。
  • 测试分支相互独立、全面覆盖。
  • 测试单元粒度足够小,函数单一职责。(要求函数体足够小,这样就比较简单的提升覆盖率,也符合函数设计的单一职责。)

对于资金型服务,覆盖率可能要求达到80%以上

单元测试-依赖

图片.png 我们的单测需要保证稳定性和幕等性,稳定是指相互隔离,能在任何时间,任何环境,运行测试。幂等是指每一次测试运行都应该产生与之前一样的结果。而要实现这一 目的就要用到mock机制。

单元测试-文件处理

图片.png

单元测试- Mock

快速Mock函数

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

图片.png

图片.png

基准测试

图片.png Go语言还提供了基准测试框架,基准测试是指测试一段程序的运行性能及耗费CPU的程度。而我们在实际项目开发中,经常会遇到代码性能瓶颈,为了定位问题经常要对代码做性能分析,这就用到了基准测试。使用方法类似于单元测试.

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

这里举一个服务器负载均衡的例子,首先我们有10个服务器列表,每次随机执行select函数随机选择一 个执行。

代码如下:

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)]
}
基准测试-优化

github.com/bytedance/g…

func FastSelect() int {
 return ServerIndex[fastrand.Intn(10)]
}

而公司为了解决这一随机性能问题, 开源了一个高性能随机数方法fastrand, 下面有开源地址;我们这边再做一下基准测试, 性能提升了百倍。

主要的思路是牺牲了一定的数列一致性,在大多数场景是适用的。

4.项目实战

目标:通过项目需求,需求拆解,逻辑设计,代码实现带领大家感受下真实的项目开发。

步骤:需求设计——代码开发——测试运行 需求设计:调查用户需求,总结设计方向

分层结构:

图片.png

5.课后实践

1)支持对话题发布回帖 2)回帖id生成需要保证不重复、唯一性。 3)新加回帖追加到本地文件,同时需要更新索引,注意Map的并发安全问题。

小结:

收获了很多知识,期待接下来的学习。