03 Go语言入门 - 工程实践 | 青训营

74 阅读6分钟

1 语言进阶

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

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

1.1 Goroutine

Go语言实现高并发的一个基本概念是协程。协程实质上是轻量级的线程,本身的调度由Go语言自身机制来完成。线程本身属于系统内核,线程可以跑多个协程。

协程的栈为KB级别,而线程的栈为MB级别。

想要开启一个协程,只需要在函数执行前添加一个go关键字即可。

1.2 CSP (Communicating Sequential Process)

中译名为通信顺序处理。Go语言提倡通过通信共享内存而非通过共享内存实现通信。下图是通信过程的示意图。

image.png

1.3 Channel(通道)

具体操作:make(chan 元素类型, [缓冲大小])

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

无缓和有缓的区别:无缓没有延迟,有缓有一定的延迟。示意图如下图所示。 image.png

1.4 并发安全 Lock

实际上Go语言也保留了通过共享内存实现通信的机制。如果不对临界区加以保护,在多线程执行时可能会出现冲突,音系我们可以使用lock函数对其进行保护。

1.5 WaitGroup

Go语言中使用WaitGroup来实现并发任务的同步。函数名:wg.

WaitGroup暴露出来三个方法:

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

2 依赖管理

这里的依赖指的是各种开发包,也即SDK。

2.1 Go 依赖管理的演进

实际上,Go语言的依赖大致可以分为三个阶段:

  • GOPATH
  • Go Vendor
  • Go Module 当前应用最广泛的是Go Module。

2.1.1 GOPATH

GOPATH是Go语言支持的一个环境变量,可被看作是Go语言的工作区。

在$GOPATH下存放三个目录:

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

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

2.1.2 Go Vendor

改进的依赖管理在项目的目录下增加了vendor文件夹,所有的依赖包以副本的形式存放在$ProjectRoot/vendor目录下。此时项目寻找依赖会优先从vendor目录下去找,如果这里没有会回到GOPATH去找。

实际上,Go Vendor是通过每个项目引入一个依赖的副本来解决多版本冲突的问题。但是,又会存在大包依赖的两个小包中版本不兼容的问题。

2.1.3 Go Module

是Go语言官方推出的依赖管理系统,目标是定义版本规则和管理项目的依赖关系。Go Module通过go.mod文件管理依赖包的版本,go get/go mod指令工具来管理依赖包。

2.2 依赖管理三要素

Go语言实现依赖管理离不开以下三个方面:

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

注意: 假如某A项目依赖了两个子项目,这两个子项目同时依赖了某B项目的不同版本,那么在最终编译时选择的某B项目版本是最低的兼容版本

在构建依赖时,如果选择直接从代码托管平台(Github)上下载源码,存在着一定的问题:

  • 无法保证构建的稳定性
  • 无法保证依赖的可用性
  • 可能增加第三方平台的压力 因此,我们引入Proxy来缓存相应的依赖。

Go语言通过配置GOPROXY环境变量来管理Proxy。实质上是URL列表,查找时会线性查找。

go get工具默认会拉取major版本的最新提交。go mod工具中,init用于初始化创建go.mod文件,download会下载模块到本地缓存,tidy可以增加需要的依赖且删除不需要的依赖。

3 测试

测试一般分为三种类型:回归测试、集成测试和单元测试。

  • 回归测试:直接应用主流场景下的测试
  • 集成测试:系统功能维度的测试
  • 单元测试:开发者对单独的函数模块进行测试 从上到下:测试成本逐渐降低,覆盖率逐渐上升。

3.1 单元测试

单元测试包含以下部分:

  • 输入
  • 测试单元
    • 函数
    • 模块等
  • 输出
  • 与期望输出的校对 单元测试可以保证工程的质量,也可以提升修改bug的效率

3.1.1 规则

  • 所有测试文件均以_test.go结尾
  • 函数命名:func TestXxx(*testing.T)
  • 初始化逻辑放到函数TestMain中:
    • 测试前进行数据装载、配置初始化等前置工作
    • 使用code := m.Run()执行测试
    • 测试后进行释放资源等收尾工作 依据上述格式编写的单元测试代码,在GoLand里面是可以被识别出来的,并且可以实现自动配置。

3.1.2 代码覆盖率

是单元测试的衡量标准。

提升方法:采用尽可能多的测试用例进行测试。

一些小Tips:

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

3.2 依赖

单元测试中的Mock是一种用于模拟(或替代)被测试代码中的依赖项的技术,可以确保测试过程的隔离性和可控性。

常用的Mock测试框架为monkeymonkey 是 Go 语言中的一个Mock框架,它允许你在运行时修改函数或方法的行为,以便在单元测试中模拟外部依赖,且不需要修改原始代码。这种方法通常被称为 "Monkey Patching",而 monkey 就是为了实现这个目标而创建的工具。它提供了一种非侵入式的方式来修改函数的行为,通过在运行时动态地替换函数的实现,来实现对函数的模拟。利用monkey可以摆脱对测试文件的依赖。

3.3 基准测试

基准测试的测试函数开头为Benchmark

为了提高基准测试的并行性能,可以选用函数fastrand。但是该函数损失了随机数列的一致性。

4 项目实战:社区话题页面的web服务实现

4.1 需求描述

  • 展示话题(标题、文字描述)和回帖列表
  • 仅实现本地的Web服务
  • 话题和回帖数据用文件形式存储

4.2 需求分析

访问页面时会涉及到两个元素:TopicPostList。二者是一对多的关系。以下是一个ER图(Entity Relationship Diagram):

image.png

4.3 分层结构

这个项目可以划分为三个层面:

  • 数据层:数据Model,实现外部数据的增删改查
  • 逻辑层:业务Entity,处理核心业务的逻辑输出
  • 视图层:视图view,处理和外部的交互逻辑,通过API的方式输出处理结果 具体的分层结构如下图:

image.png