Go工程进阶 | 青训营笔记

79 阅读9分钟

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

一、本堂课重点内容

  • Go语言进阶
  • Go依赖管理
  • Go测试
  • Go项目实战

二、详细知识点介绍

Go语言进阶

并发 VS 并行

并发:多个程序在一个核上跑

并行:多个程序在多个核上跑

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

Goroutine

goroutine(也称为协程)是go语言所提出的一种并发机制。相比于线程(一般在内核中实现),协程(一般在用户空间实现)的开销更小,栈空间一般为KB级别,而线程一般为MB级别,一个线程对应多个协程。因此协程也被称为轻量级线程

CSP并发模型

go语言提倡以通信来实现共享内存,而不是以共享内存实现通信。传统的并发模型通常以共享内存的形式加上锁的机制来实现通信。go语言在传统并发模型基础上,提出了以goroutine+channel的CSP并发模型。

Channel

If goroutines are the activities of a concurrent Go program, channels are the connections between them.

以上这句话引用自 《The Go Programming Language》 ,即所谓的“GO语言圣经”。可以说这句话很形象的解释了Channel是作为连接两个goroutine的媒介。

可以使用以下方式创建Channel:

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

其中有缓冲通道可以很好的解决读写速度不匹配的问题。

并发安全Lock

一般不使用,效率较低。

WaitGroup

常用于实现多个goroutine之间的同步。其中包含三个方法:

Add(delta int)    //计数器
Done()            //计数器-1
Wait()            //阻塞直到计数器为0

在实际使用时,常在主线程使用wg.Add(N)将计数器加到N,然后在每个goroutine结束时中使用defer wg.Done()来将计数器减一,而在主线程中在使用wg.Wait()来等待各goroutine结束。

Go依赖管理

背景

实际的工程项目,不可能从0到1搭建所有组件,我们更多是关注业务层面的实现,而其他的依赖一般都会通过SDK的方式来引入,因此对于依赖的管理是非常重要的。

Go依赖管理的演进

Go的依赖管理主要经历了三个阶段分别是GOPATH、Go Vender、Go Module,整个演进路线主要围绕实现两个目标来迭代发展,分别是:

  • 不同环境(项目)依赖的版本不同
  • 控制依赖库的版本

GOPATH

GOPATH是go语言支持的一个环境变量,其值是Go项目的工作区,工作区下有三个目录:

  • bin存放Go项目编译生成的二进制文件
  • pkg存放编译的中间产物,加快编译速度
  • src存放Go项目的源码,go get下载的包将存放至此

GOPATH的弊端:src下只能存在一个版本的包。因此如果有两个依赖不同版本的包的项目,那么就无法保证编译通过。

Go Vender

为了解决GOPATH的问题,出现了Go Vender,Vender是位于项目目录下的一个文件夹(项目位于src目录下),该文件夹中存放的是当前项目的依赖副本。在该机制下,如果存在vender文件夹,将会优先使用该文件夹下的依赖,若不存在则从GOPATH中寻找。

Go Vender的弊端:vender不能很清晰的标识依赖的版本概念。假如有一个项目依赖于包B和包C,而包 B和包C都依赖于同一个包D,但是包B和包C所依赖的包D版本不同,这样就无法控制依赖的版本(vender中同样也只能存在一个唯一版本的包),此外若更新最新版,则又有可能导致编译错误。

Go Module

Go Module是Go官方推出的依赖管理系统,解决了之前的依赖管理系统的诸多问题。从Go1.11开始Go Module就被实验性的引入,到了1.16默认开启,也被叫做go mod。

使用go mod实现依赖管理有三要素

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

配置文件由三部分组成,分别是模块路径(模块存在的标识)、原生库(标识go版本号)、单元依赖(描述该模块的依赖,其由模块路径+版本两部分唯一标识)。

对于之前两种方式来说,都没有版本规则的概念,而go mod则为了方便管理而定义了版本规则,分为语义化版本基于commit伪版本,其中语义化版本包含三部分,${MAJOR}.${MINOR}.${PATCH},major标识主版本号,不同的主版本号之间一般不兼容,minor标识次要版本号,主要用于新增功能,patch一般用于标识补丁。而基于commit伪版本其基本样式为vX.0.0-yyyymmddhhmmss-abcdefgh1234,前面的基础版本号和语义化版本相同,后面是时间戳(提交时间),最后是校验码信息(12位哈希前缀)。

单元依赖中,依赖行最后以indirect标识的依赖是非直接依赖,也就是没有直接导入该依赖的包。在依赖的模块路径中,主版本号大于2的通常会在模块路径后添加/vN后缀,N表示大于2的主版本号,对于一些在提出go mod之前主版本号就已经大于2的,为了兼容,会在版本号后加上incompatible

为了解决之前的Go Vender问题,对于同一大版本下的两个不同版本的包,可以选择最低的兼容版本,这样保证两个包都能正常运行。而对于不同大版本下的包,则在远程仓库内部建立了多个目录,多个不同的模块之间使用不同的go.mod文件,文件中引用不同版本的依赖即可。

  1. Proxy : 中心仓库管理依赖库

解决了依赖的配置问题,现在解决依赖从哪获取的问题,github是常见的代码托管平台,不过,其不适合作为管理依赖的平台,因为软件作者可以随时在代码平台增删改软件的版本,从而导致下次使用时无法保证依赖可用。而go proxy就是用来解决该问题的,go proxy是一个服务站点,其缓存了源站(代码原始网站)中的内容,版本不会改变,因此源站删除后依旧可用。

go proxy通过GOPROXY环境变量来控制,该环境变量是一个URL列表,往往在列表最后会加上direct,用来表示源站,寻找依赖时,往往会从前往后依次寻找。

  1. go get/mod : 本地工具

go get工具用于从远程获取并安装好依赖,一般的使用方法如下:

go get example.org/pkg

还可以添加一些后缀:

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

go mod 工具用于包管理,一般使用方法如下:

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

提前之前,执行go mod tidy可以有效减少无效依赖包的拉取。

Go测试

测试是实际工程开发中的一项重要内容,主要包括单测规范测试mock以及基准测试

测试一般分为回归测试集成测试单元测试。从前往后测试的成本逐渐降低,而测试的覆盖率(测试中所运行到的代码占总体代码的比例)却逐步上升。其中回归测试一般是质量保证(QA)通过终端回归一些固定的使用场景,集成测试主要是对系统的功能维度做测试验证,单元测试则是测试开发阶段,对单独函数、模块进行功能性验证。因此单元测试一定程度上决定了代码的质量。

单元测试

单元测试主要包括输入、测试单元、输出以及校对,单元的概念包括函数、接口、模块等,最后的校对用来保证功能与预期相符合。

单元测试的规则

  • 所有测试文件以_test.go结尾
  • 测试函数声明为func TestXxx(t *testinf.T)注意Test后的第一个单词要大写
  • 初始化逻辑放入func TestMain(m *testing.M)
  • 使用go test [flags] [packages]来运行测试

单元测试-Mock

在项目中往往会有很多复杂的依赖,而单元测试需要保证稳定性幂等性。稳定性是指相互隔离,无论什么时候,什么环境,运行测试结果都应该相同。而幂等性指的是每一次测试都应该与之前结果一样。要实现上述两种目的就要使用mock机制

mock指的就是对函数或实例进行模仿,反射,指针赋值。monkey就是一个开源的mock测试库,monkey有一个Patch函数可以对函数打桩,其在Runtime时,通过unsafe包将内存中函数的地址替换为运行时函数的地址。

基准测试

基准测试是指测试一端程序的运行性能以及耗费CPU的程度。使用方法与单元测试类似,此处不再多介绍。

三、实践练习例子

这次的课程讲解了掘金社区话题页面的服务端接口实现过程。主要包括:

  • 展示话题(标题,文字描述)和回帖列表
  • 暂时不考虑前端页面的实现,仅仅实现一个本地web服务
  • 话题和回帖数据使用文件存储

需求用例

用户浏览页面,页面包括两大部分,一部分是Topic,另一部分是PostList。

E-R关系

此处定义了两个实体模型

erDiagram
Topic ||--o{ Post : I
Topic {
   int64 id
   string title
   string content
   int64 create_time
}
Post {
    int64 id
    int64 topic_id
    string content
    int64 create_time
}

分层结构

整体分为三层,数据层、逻辑层和视图层

数据层关联底层数据模型,封装外部数据的增删改查,数据层面向逻辑层,对其透明,屏蔽下游数据差异。

逻辑层处理核心业务逻辑,计算打包业务实体,对于我们的需求就是Topic和PostList,并上送给视图层。

视图层负责处理和外部交互的逻辑,对于我们的需求就是封装JSON格式的请求结果,以api的形式访问。

组件工具

  • 高性能Gin框架
  • Go Mod

四、课后个人总结

通过此次课程,我了解到许多工程开发技巧,既有高性能的并发编程,也有保证代码质量的单元测试,还有重要的依赖管理,最后还通过一个实战例子将所学的知识运用了起来,对我来说,这次课程收获很大。

五、引用参考