day02Go语言进阶 | 青训营

85 阅读10分钟

01.语言进阶

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

01.并发VS并行

Go可以充分发挥多核优势,高放运行

1.1 Goroutine

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

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

1.2 CSP (Communicating Sequential Processes)

提倡通过通信共享内存而不是通过共享内存而实现通信

1.3 Channel

make(chan元素类型,[缓冲大小])

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

02.依赖管理

“背景Go | 依赖管理演进 | Go Module实践

了解GO语言依赖管理的演进路线。

背景

对于hello world以及类似的单体函数只需要依赖原生SDK,而实际工程会相对复杂,我们不可能基于标准库0~1编码搭建,而更多的关注业务逻辑的实现,而其他的涉及框架、日志、driver、 以及collection等一 系列依赖都会通过sdk的方式引入,这样对依赖包的管理就显得尤为重要。

2.1Go依赖管理演进

而Go的依赖管理主要经历了3个阶段,分别是,GOPATH-->Go Vendor-->Go Module 。到目前被广泛应用的go module,整个演进路线主要围绕实现两个目标来迭代发展的

2.1.1 GOPATH

GOPATH是Go语言支持的一个环境变量,value是GO项目的工作区。目录有以下结构:

  1. bin:存放Go项目编译生成的二进制文件。
  2. pkg:存放编译的中间产物,加快编译速度。
  3. src:存放Go项目的源码:。

2.1.1 GOPATH-弊端

如图,同一个pkg,有2个版本,A->A0,B->B0,而src下只能有一 个版本存在, 那AB项目无法保证都能编译通过。也就是在go path管理模式下, 如果多个项目依赖同一个库, 则依赖该库是同一份代码,所以不同项目不能依赖同一个库的不同版本,这很显然不能满足我们的项目依赖需求。为了解决这问题,govender出现了 。

2.1.2 Go Vendor-弊端

Vendor是当前项目中的一一个目录,其中存放了当前项目依赖的副本。在Vendor机制下, 如果当前项目存在Vendor目录,会优先使用该目录下的依赖,如果依赖不存在,会从GOPATH中寻找;

但vendor无法很好解决依赖包的版本变动问题和一个项目依赖同一个包的不同版本的问题,下面我们看一 个场景

2.1.3 Go Module

  1. 通过go.mod文件管理依赖包版本
  2. 通过goget/gomod指令工具管理依赖包

GO Modules是Go语言言方推出的依赖管理系统,解决了之前依赖管理系统存在的诸如无法依赖同一个库的多 个版本等问题,go moule从Go 1.11开始实验性引入,Go 1.16默认开启;我们般都读为g mod,我们也先统下名称

2.2依赖管理三要素

对于Java选手而言就是可以类比下maven。

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

2.3.1 依赖配置- go.mod

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

2.3.2依赖配置version

go path和go vendor都是源码副本方式依赖,没有版本规则概念,而go mod为了放方便管理则定义了版本规则,分为语义化版本; 其中语义化版本包括不同的MAJOR版本表示是不兼容的AP,所以即使是同一个库,MAJOR 版本不同也会被认为是不同的模块: MINOR 版本通常是新增函数或功能,向后兼容;

而patch 版本一般是修复 bug 基础版本前缀是和语义化版本一样的;时间戳(yyymmddhhmmss),也就是提交Commit的时间,最后是校验(abcdefabcdef),包含12位的哈希前缀;每次提交commit后Go都会默认生成一个伪版本号。

2.3.3依赖配置- indirect

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

2.3.4依赖配置- incompatible

下一个常见是的是incompatible,主版本2 +模块会在模块路径增加/vN后缀,这能让go module按照不同的模块来处理同一个项目不同主版本的依赖。

由于go module是1。11实验性引入所以这项规则提出之前已经有一些仓库打 上了2或者更高版本的tag了,为了兼容这部分仓库,对于没有go.mod文件并且主版本在2或者以上的依赖,会在版本号后加上+incompatible后缀

2.3.4依赖配置依赖图


如果X项目依赖了A、B两个项目,且A、B分别依赖了C项目的v1.3、v1.4两个版本,最终编译时所使用的C项目的版本为如下哪个选项? (单选)

A. v1.3

B. v1.4

C. A用到C时用v1. 3编译, B用到C时用v1.4编译

答案:B, 选择最低的兼容版本

2.3.5依赖分发-回源

“go module的依赖分发。也就是从哪里下载,如何下载的问题?

github是比较常见给的代码托管系统平台,而Go Modules系统中定义的依赖,最终可以对应到多版本代码管理系统中某一项目的特定提交或版本, 这样的话,对于go.mod中定义的依赖,则直接可以从对应仓库中下载指定软件依赖,从而完成依赖分发。

但直接使用版本管理仓库下载依赖,存在多个问题,首先无法保证构建确定性:软件作者可以直接代码平台增加修改/删除软件版本,导致下次构建使用另外版本的依赖,或者找不到依赖版本。无法保证依赖可用性:依赖软件作者可以直接代码平台删除软件,导致依赖不可用;大幅增加第三方代码托管平台压力。

2.3.5依赖分发-Proxy

而go proxy就是解决这些问题的方案,Go Proxy是一个服务站点, 它会缓源站中的软件内容,缓存的软件版本不会改变,并且在源站软件删除之后依然可用,从而实现了供"immutability"和"available”的依赖分发;使用Go Proxy之后,构建时会直接从Go Proxy站点拉取依赖。类比项目中,下游无法满足我们上游的需求

2.3.6依赖分发-变量GOPROXY

GOPROXY="proxy1.cn, proxy2.cn ,direct'

服务站点URL列表,“direct" 表示源站

go proxy的使用: Go Modules通过GOPROXY环境变量控制如何使用Go Proxy; GOPROXY是一个Go Proxy站点URL列表,可以使用directr表示源站。对于示例配置,整体的依赖寻址路径, 会优先从proxy1下载依赖, 如果proxy1不存在,后下钻proxy2寻找,如果proxy2, 中不存在则会回源到源站直接下载依赖,缓存到proxy站点中。

2.3.7工具- go get

2.3.8工具-go mod

尽量提交之前执行下go tidy,减少构建时无效依赖包的拉取

依赖管理二要素

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

02小结

  1. Go依赖管理演进
  2. Go Module依赖管理方案

03.测试

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

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

事故

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

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

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

3.1单元测试

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

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

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

3.1.1单元测试-规则

从文件上就很好了区分源码和测试代码,以Test开头, 且连接的第一个字母大写

3.1.2单元测试例子

func HelloTom() string {
 return "Tom"
}

复制

测试代码

func TestHelloTom(t *testing.T) {
 output := HelloTom()
 expectOutput := "Tom"
 assert.Equal(t, expectOutput, output)
}

复制

3.1.3单元测试-运行

go test [flags] [packages]

3.1.4单元测试- assert

package test

import (
 "github.com/stretchr/testify/assert"
 "testing"
)

func TestHelloTom(t *testing.T) {
 output := HelloTom()
 expectOutput := "Tom"
 assert.Equal(t, expectOutput, output)
}

复制

3.1.5单元测试-覆盖率

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

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

3.2单元测试-依赖

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

3.3单元测试-文件处理

package test

import (
 "bufio"
 "os"
 "strings"
)

func ReadFirstLine() string {
 open, err := os.Open("log")
 defer open.Close()
 if err != nil {
  return ""
 }
 scanner := bufio.NewScanner(open)
 for scanner.Scan() {
  return scanner.Text()
 }
 return ""
}

func ProcessFirstLine() string {
 line := ReadFirstLine()
 destLine := strings.ReplaceAll(line, "11", "00")
 return destLine
}

复制

测试类

package test

import (
 "bou.ke/monkey"
 "github.com/stretchr/testify/assert"
 "testing"
)

func TestProcessFirstLine(t *testing.T) {
 firstLine := ProcessFirstLine()
 assert.Equal(t, "line00", firstLine)
}

func TestProcessFirstLineWithMock(t *testing.T) {
 monkey.Patch(ReadFirstLine, func() string {
  return "line110"
 })
 defer monkey.Unpatch(ReadFirstLine)
 line := ProcessFirstLine()
 assert.Equal(t, "line000", line)
}

复制

3.4单元测试- Mock

monkey : github.com/bouk/monkey

快速Mock函数

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

import (
 "bou.ke/monkey"
 "github.com/stretchr/testify/assert"
 "testing"
)

func TestProcessFirstLine(t *testing.T) {
 firstLine := ProcessFirstLine()
 assert.Equal(t, "line00", firstLine)
}

func TestProcessFirstLineWithMock(t *testing.T) {
 monkey.Patch(ReadFirstLine, func() string {
  return "line110"
 })
 defer monkey.Unpatch(ReadFirstLine)
 line := ProcessFirstLine()
 assert.Equal(t, "line000", line)
}

复制

3.5基准测试

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

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

3.5.1基准测试例子

这里举一个服务器负载均衡的例子,首先我们有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)]
}

复制

3.5.3基准测试-优化

github.com/bytedance/g…

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

复制

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

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