GO语言入门-工程实践 | 青训营笔记

115 阅读8分钟

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

1 语言进阶(并发编程)

1.1 并发 VS 并行

  • 并发:多线程程序在一个核的cpu上运行
  • 并行:多线程程序在多个核的cpu上运行

1.2 Goroutine(协程)

  • 线程和协程的区别:
    • 线程:用户态,轻量级线程,栈MB级别
    • 协程:内核态,线程跑多个协程,栈KB级别
  • 使用go func来开启一个goroutine,示例: 请添加图片描述

1.3 CSP(communicating sequential processes)并发模型

  • CSP并发模型是描述两个独立的并发实体通过共享 channel(管道)进行通信的并发模型。Go语言实现了CSP模型中process和channel这两个概念。process就是Go语言中的goroutine,每个goroutine之间是通过channel通讯来实现数据共享。
  • 提倡通过通信共享内存,而不是通过共享内存而实现通信,因为需要对临界区加锁,对程序性能会有一定影响: 请添加图片描述

1.4 Channel 通道

  • Channel是一种引用类型,创建Channel的形式为:make(chan 元素类型, [缓存大小]),分为两种:
    • 无缓冲通道 make(chan int),也被称为同步通道,发送和接收的goroutine同步存取
    • 有缓冲通道 make(chan int,2),通道中可以存放元素,是一个典型的生产者消费者模型,可以解决生产和消费速率不均衡带来的效率问题 在这里插入图片描述

1.5 并发安全 Lock

  • 并发锁相关的头文件为sync
  • 并发锁的类型为sync.Mutex
  • 假设有一个sync.Mutex锁类型的变量lock,加锁操作为lock.Lock(),解锁操作为lock.Unlock()

1.6 WaitGroup

  • WaitGroup就是package sync用来做任务编排的一个并发原语。解决的就是并发-等待的问题,提供了三个方法:
    • Add(delta int),用来设置WaitGroup的计数值;
    • Done(),用来将WaitGroup的计数值减1,其实就是调用了Add(-1);
    • Wait(),调用这个方法的goroutine会一直阻塞,直到WaitGroup的计数值变为0。 在这里插入图片描述

2 依赖管理

2.1 Go依赖管理演进

GOPATH->GO Vendor->Go Module

  • GOPATH:是Go语言支持的一个环境变量,是Go项目的工作区
    • 目录下主要有三个文件夹:
      • src:存放Go项目的源码
      • pkg:项目编译的中间产物,加快编译速度
      • bin:项目编译的二进制文件夹
    • 弊端:无法实现package的多版本控制
  • Go Vendor:项目目录下增加vendor文件夹,其中存放了当前项目依赖的副本
    • 如果当前项目存在Vendor目录,会优先使用该目录下的依赖,如果依赖不存在,则会从GOPATH中寻找
    • 弊端:无法很好解决依赖包的版本变动问题和一个项目依赖同一个包的不同版本的问题
  • Go Module:是Go语言官方推出的依赖管理系统,解决了之前无法依赖同一个库的多个版本等问题,定义版本规则和管理项目依赖关系
    • 通过go.mod文件管理依赖包版本
    • 通过go get/go mod指令工具管理依赖包

2.2 依赖管理三要素

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

2.3 依赖配置

2.3.1 go.mod

在这里插入图片描述

  • 从上到下分为三个部分:
    • 最上面module开头的是依赖管理基本单元,模块路径可以用来标识一个模块,表示可以从哪里找到该模块,如果是github前缀,那么表示可以从Github仓库找到该模块,依赖包的源代码由github托管,如果项目的子包想被单独引用,则需要通过单独的init go.mod文件进行管理
    • 中间是依赖的原生sdk版本
    • 最下面是单元依赖,每个依赖单元用模块路径+版本来唯一标示

2.3.2 version

  • 语义化版本:${MAJOR}.${MINOR}.${PATCH}
    • 不同的MAJOR版本表示是不兼容的API,所以即使是同一个库,MAJOR版本不同也会被认为是不同的模块
    • MINOR版本通常是新增函数或功能,向后兼容
    • PATCH版本一般是修复bug
    • 示例:V1.3.0,V2.3.0
  • 基于commit伪版本,有前缀、时间戳、校检码三个部分
    • 前缀和语义化版本一样
    • 时间戳(yyyymmddhhmmss)就是提交Commit的时间
    • 校检码包含12位的哈希前缀,每次提交commit后Go都会默认生成一个伪版本号
    • 示例:vX.0.0-yyyymmddhhmmss-abcdefgh1234

2.3.3 indirect

indirect后缀表示go.mod对应的当前模块没有直接导入该依赖模块的包,也就是间接依赖,比如A->B->C,A->B表示直接依赖,A->C表示间接依赖

2.3.4 incompatible

  • 主版本2+模块会在模块路径增加/vN后缀,这能让go module按照不同的模块来处理同一个项目不同主版本的依赖
  • 对于没有go.mod文件并且主版本2+的依赖,会在版本号后加上+incompatible后缀

2.3.5 依赖图

在这里插入图片描述

如果X项目依赖了A、B两个项目,且A、B分别依赖了C项目的v1.3、v1.4两个版本,最终编译时所使用的C项目的版本为最低的兼容版本,即v1.4

2.4 依赖分发

2.4.1 回源

  • gomodule的依赖分发,就是从哪里下载,如何下载的问题
  • Go Modules系统中定义的依赖,最终可以对应到多版本代码管理系统中某一项目的特定提交或版本,对应go.mod中定义的依赖,可以直接从对应仓库中下载知道软件依赖,完成依赖分发
  • 存在问题:
    • 无法保证构建确定性,软件作者可以直接在代码平台修改软件版本,导致下一构建时使用的版本依赖不同
    • 无法保证依赖可用性:软件作者可以直接在代码平台删除软件,导致依赖不可用
    • 增加第三方代码托管平台的压力

2.4.2 Proxy

  • Go Proxy是一个服务站点,会缓存源站中的软件内容,软件版本不会改变,即使源站软件删除了仍然可用
  • 构建时直接从Go Proxy站点拉取依赖,比较稳定可靠
  • Go Modules通过GOPROXY环境变量控制使用Go Proxy,GOPROXY是一个Go Proxy站点URL列表,用“direct”表示源站
  • 对于示例:GOPROXY="proxy1.cn, proxy2.cn, direct",会优先从proxy1下载依赖,不存在则从proxy2进行寻找,不存在则回到源站直接下载依赖,然后缓存到proxy站点

2.5 工具

2.5.1 go get

  • go get命令使用语句从指定网站下载依赖:go get example.org/pkg,后面可以加上后缀:
    • @update表示默认
    • @none表示删除依赖
    • @v1.1.2表示tag版本,语义版本
    • @23dfdd5表示特定的commit
    • @master表示分支的最新commit

2.5.2 go mod

  • go mod命令语句后面可以加上后缀:
    • init表示初始化,创建go.mod文件
    • download表示下载模块到本地缓存
    • tidy表示增加需要的依赖,删除不需要的依赖

3 测试

3.1 单元测试

单元测试主要包括输入、测试单元、输出、以及校对,单元包括接口、函数、模块等,用最后的校对来保证代码的功能与我们的预期相符。单元测试一方面可以保证质量,另一方面可以提升效率。

3.1.1 规则

  • 所有测试文件以_test.go结尾,很好区分源码和测试代码
  • 测试函数形式以Test开头,且连接的第一个字母大写,func TestXxx(t *testing.T)
  • 初始化逻辑放到TestMain中
  • 运行命令:go test [flags] [packages]
  • 使用assert方法来判断结果是否正确,比如assert.Equal(t, expectOutput, output),方法依赖为"github.com/stretchr/testify/assert"

3.1.2 覆盖率

  • 在运行命令后面加上--cover选项即可显示覆盖率
  • 一般覆盖率:50%~60%,较高覆盖率80%+
  • 测试分支相互独立、全面覆盖
  • 测试单元粒度足够小,函数单一职责

3.1.3 Mock

  • 依赖库:github.com/bouk/monkey
  • 可以对method,或者实例的方法进行mock
  • Patch方法进行打桩,Unpatch方法进行卸桩,作用域在Runtime,在运行时通过Go的unsafe包,能够将内存中函数的地址替换为运行时函数的地址,通过defer卸载mock,这样测试函数就可以摆脱本地文件的束缚和依赖,示例: 在这里插入图片描述

3.2 基准测试

  • 基准测试是指测试一段程序的运行性能及耗费CPU的程度,为了优化代码经常要对代码进行性能分析,Go内置的测试框架提供了基准测试的能力
  • 基准测试方法以Benchmark开头,入参是testing.B
  • 运行命令:go test -bench=.

4 个人思考

Go语言的高性能主要体现在对并发编程的许多支持上。Go语言使用了goroutine协程、CSP并发模型等,使多个并发实体可以通过管道进行通信,提高了并发效率,在并发安全方面,有锁Lock、WaitGroup等进行保证。在依赖管理方面,目前使用的依赖管理系统是Go Module,解决了之前无法依赖同一个库的多个版本等问题,定义了版本规则和管理项目依赖关系。在测试方面,Go语言也有较为完善的工具,可以帮助进行单元测试、Mock、基准测试等。