网络优化与网络稳定 | 青训营笔记

112 阅读10分钟

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

本文分享Go语言的上手进阶以及工程实践

01.语言进阶

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

01.并发VS并行

并发:多线程在一个核的CPU上运行;并行:多线程程序在多个核的CPU上运行 Go可以充分发挥多核优势,高效运行。Go语言可以说是为高并发而生的。

1.1 Goroutine

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

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

实际开发过程中用go 开发协程

1.2 CSP(Communicating Sqeuential Processes)协程之间的通信

Go语言提倡通过通信共享内存,而不是通过共享内存来实现通信

涉及到一个概念,通道channel

goroutine 是程序并发的执行体

1.3 Channel

Channel 是一种引用类型,创建需要用到make关键字, make(chan 元素类型,[缓冲大小]),根据缓冲大小,可分为有缓冲通道和无缓冲通道

使用无缓冲通道进行通信,会导致发送的goroutine和接收的goroutine同步化,因此无缓冲通道也被称为同步通道。

解决同步问题的一个方式,就是使用带有缓冲区的通道,大小就是通道可存储的元素数量。

带缓冲的channel 能解决生产和消费速度不均衡带来的执行效率问题

1.4 并发安全 Lock

通过lock获取临界区资源,计算完成后再将临界区释放掉 func Add ,不加锁输出不一定为10000,加锁一定为10000,结论:不加锁输出未知结果,这就是并发安全问题,属于 undefined 行为。

如何解决,就是加锁!,通过对临界区权限的控制,来保证并发安全。 并发安全问题往往难定位。

1.5 WaitGroup

前面的例子都用sleep实现暴力的阻塞,这肯定不优雅, Go语言可以通过waitgroup来实现并发任务的同步,

三个方法:Add(delta int) 计数器+delta ; Done() 计数器-1 ; Wait()阻塞知道计数器为0 内部就是维护一个计数器,例如启动了n个并发任务,可add(n), 每个任务完成时,调用Done方法将计数器-1,最后,调用wait方法来阻塞,等待所有的并发任务执行完。 当计数器值为0时,就代表了所有的并发任务已经完成。

小结

Goroutine:go可以通过高效的调度模型来实现协程的一个高并发的操作

Channel:通过通信实现共享内存

Sync:关键字mutex,waitgroup,主要是为了实现并发安全操作和协程的同步

02.依赖 管理

了解Go语言依赖管理的演进路线, 背景|Go依赖管理演进|Go Module实践

hello 用原生的SDK就可以 SDK:Software Development Kit 软件开发工具包

实际开发工程会相对复杂,不可能基于标准库,从0-1编码搭建,我们要把精力放在业务逻辑上,

2.1 Go 依赖管理演进

GOPATH、Go Vendor、Go Module

围绕两个关键:1. 不同环境(项目)依赖的版本不同; 2.控制依赖库的版本

2.1.1 GOPATH

GOPATH Go语言的环境变量,是一个go项目的工作区

bin 项目编译的二进制文件 pkg 项目编译的中间产物,加速编译 src 项目源码

项目代码直接依赖 src下的代码

go get 下载最新版本的包到src目录下

GOPATH 的弊端 project A 和 B 依赖与某一 package 的不同版本。 问题:无法实现package 的多版本控制

2.1.2 Go Vender 项目目录下增加了vender文件,所有依赖包副本形式放在ProjectRoot/vendor 依赖寻址方式:vendor-> GOPATH

通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题

问题,project A 依赖 B 和 C,B依赖D的v1版本,C依赖D的v2版本,无法很好地控制v1和v2版本的选择问题。 可能出现冲突,导致编译错误

问题:

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

2.1.3 Go Module (Go语言推出的依赖管理系统)

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

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

2.2 依赖管理三要素

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

2.3.1 依赖配置 go.mode中第一行为 依赖管理基本单元 第三行为go原生库版本 之后为单元依赖,每个依赖由两部分组成,一是mod path,后面跟版本号,这样就可以唯一定位某个仓库的某个版本 依赖标识:Module Path

2.3.2

2.3.3 依赖配置-indirect

indirect代表间接依赖 比如:a ->b -> c,a 对 b 就是直接依赖,a对c就是间接依赖

2.3.4 依赖配置-incompatible

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

2.3.4 依赖配置-依赖图

2.3.5 依赖分发-回源

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

goproxy 是一个服务站点,会缓存原站中的软件内容

2.3.7 工具 - go get

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

go mod

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

小结

  • Go依赖管理演进
  • Go Module 依赖管理方案

03.测试

从单元测试实践除法,提升大家的质量意识 单元测试|Mock测试|基准测试

事故:

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

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

回归测试,集成测试,单元测试 从上到下,覆盖率逐层变大,成本却逐层降低

单元测试主要包括,输入,测试单元,输出,以及校对,单元的概念比较广,包括接口,函数,模块等;用最后的校对来保证代码的功能与我们的预期相符;单侧一方面可以保证质量,在整体覆盖率足够的情况下,一定程度上既保证了新功能本身的正确性,又未破坏原有代码的正确性。另一方面可以提升效率,在代码有bug的情况下,通过编写单测,可以在一个较短周期内定位和修复问题。

3.1.1 单元测试-规则

3.1.5 单元测试-覆盖率

go test judgment_test.go judgment.go --cover 得到覆盖率,被测试函数运行的行数百分比

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

3.2 单元测试-依赖

幂等是指每一次测试运行都应该产生与之前一样的结构,而要实现这一目的就要用到mock机制。

3.3 单元测试-文件处理

3.5 基准测试

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

举个服务器负载均衡的例子

小结

单元测试 Mock测试 基准测试

04.项目实战

通过项目需求、需求拆解、逻辑设计、代码实现带领大家感受下真实的项目开发 需求设计|代码开发|测试运行

需求背景:掘金的社区话题入口,页面的功能包括话题详情、回帖列表、支持回帖、点赞和回帖回复,我们今天就以此为需求模型,开发一个该页面交涉

社区话题页面

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

4.4 分层结构3

  • 数据层:数据Model,外部数据的增删查改
  • 逻辑层:业务Entity,处理核心业务逻辑输出
  • 视图层:视图view,处理和外部的交互逻辑

整体分为三层,repository数据层,service逻辑层,controoler视图层, 数据层关联底层数据模型,也就是这里的model,封装外部数据的增删改查,我们的数据存储在本地文件,通过文件操作拉取话题,帖子数据;数据层面向逻辑层,对service层透明,屏蔽下游数据差异,也就是不管下游是文件,还是数据库,还是微服务等,对service层的接口模型是不变的。 Servcie逻辑层处理核心业务逻辑,计算打包业务实体entiy,对应我们的需求,就是话题页面,包括话题和回帖列表,并上送给视图层; Controller视图层负责处理和外部的交互逻辑,以view视图的形式返回给客户端,对于我们需求,我们封装json格式化的请求结果,api形式访问就好。

4.5 组件工具3

Gin 高性能 go web 框架

4.6 Repository-查询3

实现索引后就是实现查询操作。就比较简单了,根据索引直接查询即可。利用syc.Once,sync.Once 适合在高并发场景下只执行一次的情况,单例模式,可减少存储的浪费。

索引:话题ID,话题ID 数据:话题 帖子列表

有了这两个函数,我们就可以上送给逻辑层,在逻辑层进行实体entity的封装

4.7 Service33

实体:

type PageInfo struct {
   Topic    *repository.Topic
   PostList []*repository.Post
}

流程:参数校验 --> 准备数据 --> 组装实体

query_page_info.go 通过err控制整个流程的退出,正常会返回页面信息,err为nil

checkParam 做数据校验 prepareInfo,通过respos那一层获取话题数据和回帖列表数据,两者对话题信息和回帖信息并行处理,可提高执行效率(话题信息和回帖信息两者是没有相互依赖的,对这种情况,就可以考虑并行处理

4.8 Controller

Service层处理完成,下面就是Controller层,这一层的逻辑是比较简单的

  • 构建View对象
  • 业务错误码

首先,构建view对象,也就是 PageData

至此,从repository到Service到Controller,整个代码结构、框架已经实现完成,业务逻辑整体完成

4.9Router

通过通过gin搭建整个web框架,分四步:

  • 初始化数据索引
  • 初始化引擎配置
  • 构建路由
  • 启动服务

最后执行 go run 本地启动 web 服务,通过 curl 命令请求服务暴露的接口