Bootstrap

Go工程基础知识

前言

记录Go工程基础知识。

一、Go 包管理

go有三类型的包,

  1. 标准库的包,在go程序中可以直接使用。
  2. 用户自定义包,开发者自行引用。
  3. 第三方包,非标准库/非自定义包,而是第三者开发并分享到开源仓库的包,开发者可以通过包管理工具进行下载和安装。

所以,包管理就是用来管理项目对第三方包的依赖关系。

1、技术演进

包管理技术的演进过程,

  1. go path
    项目的目录结构如下,
文件夹作用
src存项目源码
pkg存项目编译的中间产物
bin存项目编译的二进制文件

所有项目的包都存在src下,当两个项目依赖同一个包的不同版本时,两个项目都是无法通过编译的,因为src不允许同时存在一个包的两个版本。

  1. go vendor
    所有项目的依赖包存入src文件中,才出现了项目无法编译通过的问题,所以为每个项目独立开辟一个“src”这样的包文件,两个项目所依赖同一个包的不同版本就会出现在不同的“src”中,就不会出现冲突导致无法通过编译。这里的“src”就是vendor子目录。
    注:如果当前项目存在vendor目录,会优先使用该目录下的依赖,如果该目录下没有,则会从go path中寻找。
    问题所在,vendor子目录也是不允许同时放一个包的两个版本,但是存在项目不同包用到了同一个第三方包的不同版本,此时项目无法通过编译。
  2. go module
  • go mod组成的四部分,
配置/工具作用
go get/go mod通过下载、更新等方式管理依赖包
go.mod 文件管理依赖包版本
go.sum 文件校验哈希值防止包被篡改
环境变量控制开关及私有化设置

注:在 Go 1.16 之前,go get可以用来下载、更新和安装第三方库,包括二进制文件;在 Go 1.16 之后,go get 只用来修改 go.mod 文件的依赖项,并下载到 $GOPATH/pkg/mod 目
录下。编译和安装的工作则交给go install完成。

2、go mod

  • go mod相关命令,如下,
Go Mod 命令作用
go mod init生成go.mod文件
go mod download下载go.mod文件中指明的所有依赖
go mod tidy整理现有的依赖,增加需要的依赖,删除不需要的依赖(因为迭代)
go mod graph查看现有的依赖结构
go mod edit编辑go.mod文件
go mod vendor导出项目所有依赖到vendor文件中
go mod verify检验一个模块是否被篡改过
go mod why查看为什么需要依赖某模块
  • Go mod环境变量,
变量名作用可选值私有化仓库设置
GO111MODULEgo module是否启用的开关auto:有mod文件就启用;
on:启用;
off:禁用;
一般为on,有些旧项目或分支会设置成auto
GOPROXY设置go模块下载的代理https://proxy.golang.org,
https://goproxy.cn,
https://mirrors.aliyun.com/goproxy/,
direct(源站)
https://mirrors.company.cn/go
GONOPROXY不需要代理的模块,配合私有仓库。通常跟随GOPRIVATE的值/
GOSUMDB设置go checksum database的地址,保证模块没有被篡改sum.golang.org
off : 关闭
off
GONOSUMDB不用校验的模块,配合私有仓库通过跟随GOPRIVATE的值/
GOPRIVATE设置私有仓库地址或者匹配规则/https://git.company.com/private
  • go mod 文件结构,
    在这里插入图片描述
    1.module 路径,表示module
    2.go 版本号
    3.依赖项:用module路径 + 版本号来唯一标识。
关键字作用
require项目所依赖项模块,带有indirect注释代表非直接依赖。
replace可替换依赖项模块,用于解决一些依赖包无法拉取,或者需要使用本地或远程修改过的模块。
exclude可忽略依赖项模块,用于避免一些有问题或者不兼容的依赖包版本。
  • 依赖版本号,
  1. 语义化版本,清晰、简洁、一致的版本号约定,使开发者和用户能够更好地理解软件版本之间的差异和兼容性。major.minor.patch-pre-release+metadata
类别含义示例
主版本号(major)1.不兼容的API修改时需要更新
2.major>1时还可以体现在模块路径中避免兼容性问题
v.9.0.2
module路径:https://github.com/redis/go-redis/tree/v9
次版本号(minor)向下兼容的新功能增加时v1.1.0
修订号(patch)向下兼容的问题修正时v1.0.1
预发布标识符(pre-release)表示预发布版本(如alpha、beta、rc等)v1.0.0-alpha
元数据(meta-data)表示与版本相关的附加信息v1.0.0+build-123
  1. 伪版本,当依赖包的仓库中没有任何语义化版本使用伪版本,v0.0.0-yyyymmddhhmmss-abcdefabcdef
类别含义示例
yyyymmddhhmmss提交时间戳(年月日时分秒)20220622213112
abcdefabcdef提交哈希值05595931fe9d
  1. 私有库版本,版本号:v1.0.0-company
  • go sum文件
    go.sum文件记录了当前项目所有模块版本和哈希值,包括直接或间接依赖的。哈希值用于检测依赖包是否被篡改。
    go.sum文件中每行记录由 module名、版本和哈希组成。

第一个hash 是 Go modules 将目标模块版本的 zip 文件解压缩后,针对所有包内文件依次进行 hash,然后再把它们的 hash 结果按照固定格式和算法组成总的 hash 值。
第二个hash,仅仅是把go.mod文件进行hash。

第二个hash必然存在,当 Go认为肯定用不到某个模块版本的时候就会省略第一
个hash
在这里插入图片描述

二、Go测试

控制软件复杂性的有效手段,即代码评审+测试。

1、单元测试

在Go中,测试文件以后缀名_test.go结尾。

单元测试是通过编写测试函数来实现的,这些函数以“Test”开头,并接受一个“*testing.T”类型的参数,
它提供了许多有用的断言函数,例如“t.Errorf”和“t.Fatalf”,用来输出不符合预期的异常情况。

通过“go test -v -run FuncName”命令来运行这些测试函数,该命令将自动查找和运行测试函数并生成测试报告
在这里插入图片描述

  • 表驱动写法
    让你的测试代码更简洁,也可以让你更容易地添加新的测试用例。
    在这里插入图片描述
    在这里插入图片描述
  • 测试覆盖率
    对程序的测试程度一般通过测试覆盖率来衡量,通常指代码行数、分支数、函数数等统计数据中被测试用例覆盖到的比例。

在运行“go test”命令时,可以使用“-cover”选项输出测试覆盖率。

如果需要显示详细的测试覆盖率报告,可以通过下面的方式,

go test -coverprofile=c.out
go tool cover -html=c.out

2、测试框架

  • HttpTest,http自带。
    针对 http 开发的场景,可以使用httptest包轻松地编写HTTP单元测试,它可以模拟请求和响应,而不需要实际连接到远程HTTP服务器。

使用httptest.NewRecorder()来测试服务器的HTTP响应。

httptest.NewRecorder()是一个实现了http.ResponseWriter接口的类型,可以用来记录处理器写入响应的数据,并在测试中进行检查。

你可以使用它来创建一个响应记录器,并将其传递给你的处理器函数,然后从记录器中获取响应结果并与预期输出进行比较。
在这里插入图片描述

  • Convey
    Convey是Go语言中的一种测试框架,它可以用于编写可读性强、可维护性强的测试代码。

Convey可以在测试代码中使用自然语言来表达测试条件和结果,并提供了一组易于使用的API,用于编写测试代码。

使用ShouldEqual函数检查两个值是否相等
使用ShouldNotBeNil函数检查值是否不为空
使用ShouldPanicWith函数检查代码是否会抛出异常等

Convey()函数用于描述测试的上下文,例如测试的功能或特性
So()函数用于描述测试的条件和结果,并在测试失败时提供详细的错误信息
在这里插入图片描述

  • GoMock
    对于外部依赖的情况,我们往往采用两种办法来解决,一种是Mock(模拟) ,一种是Stub(桩)。

Mock:是模拟的意思,指的是在测试包中创建一个对象,满足某个外部依赖的接口 interface{}。
Stub: 是桩的意思,指的是在测试包中创建一个模拟方法,用于替换生成代码中的方法。

区别项MockStub
实现原理不同替换的是实现接口的对象替换的是方法
侵入性不同mock没有侵入性stub的侵入性比较强
实现复杂度mock复杂度高,需要提前实现各个对象stub一个函数即可,更灵活

gomock 是基于 interface 的 mock 工具,可以为 interface 生成 mock 对象,并对其行为进行预设和验证。

gomock包含:gomock库和辅助代码生成工具mockgenmockgen的安装及使用
go install github.com/golang/mock/[email protected]
mockgen -source=db.go -destination=db_mock.go -package=main

类别用法说明
参数Eq(value)表示与 value 等价的值
-Any()可以用来表示任意的入参
-Not(value)用来表示非 value 以外的值
-Nil()表示 None 值
返回值Return返回确定的值
-DoMock 方法被调用时,要执行的操作,忽略返回值
-DoAndReturn可以动态地控制返回值
调用次数Times()断言 Mock 方法被调用的次数
-MaxTimes()最大次数
-MinTimes()最小次数
-AnyTimes()任意次数(包括 0 次)
调用顺序InOrder控制预设行为的执行顺序

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

  • GoMonkey
    gomonkey 是 golang 的一款打桩框架,目标是让用户在单元测试中低成本的完成打桩,从而将精力聚焦于业务功能的开发。
    它不仅可以为函数/接口打桩,也可以为全局变量、私有成员方法、结构体成员方法等打桩。

具体使用
ApplyFunc:为函数打桩。
ApplyMethod:为结构体成员方法打桩。
ApplyGlobalVar:为全局变量打桩
ApplyPrivateFunc:为私有函数打桩
ApplyPrivateMethod:为私有成员方法打桩
在这里插入图片描述

3、基准测试

a.基本概念

基准测试是测量一个程序在固定工作负载下的性能的方法。

基准测试叫基准测试,是因为它需要选定一个或多个基准,作为测试的参照物。
基准可以是某个程序或系统的性能指标,也可以是某个行业或领域的标准。
基准测试就是通过对比不同程序或系统与基准之间的差异,来评估其优劣和改进方向。

基准测试可以帮助我们优化程序性能,比较不同算法或实现方式的效率,发现程序中存在的性能瓶颈或内存泄漏等问题。

b.编写基准测试函数

基准测试函数和普通测试函数写法类似,但是以Benchmark为前缀名,并且带有一个*testing.B类型的参数。
testing.B参数除了提供和testing.T类似的方法,还有额外一些和性能测量相关的方法,它还提供了一个整数N用于指定操作执行的循环次数。
在这里插入图片描述

c.运行基准测试函数

运行基准测试需要使用go test命令,并加上-bench=表达式,用来匹配基准测试的函数。
在这里插入图片描述
重要指标

  • 每次操作执行的平均时间(ns/op)
  • 每次操作分配的内存字节数(B/op)
  • 分配对象的次数(allocs/op)

d.基准测试样例演示

在这里插入图片描述

三、编码规范

  1. 命名规范,
  • 普通变量,在尽量表达上下文(配合注释把上下文解释清楚)的时候,让变量更短;或者常识标识符,如for中的i而不是index;
  • 函数变量,尽量表达上下文的同时让标识符变短,注意不要上下文重复,比如包已经包含信息了,毕竟包和函数成对出现。如http.Serve(),而不是http.ServeHTTP().
  • package,尽量表达上下文的同时让标识符更短,注意不要和标准库同名,同时只用小写字母来标识。
    1.不使用常用变量名,如bufio而不是buf;
    2.使用单数而不是复数,除法冲突,如encoding,而不是encodings;
    3.谨慎使用缩写,除法该缩写不破坏上下文,比如fmt就是format的缩写。

小结:核心目的就是提高代码可阅读性,方便维护和debug。所以命名在考虑上下文的基础上来做到尽量简短,做不到的可以配合注释来提高代码阅读性,降低阅读成本。

  1. 控制流程,
  • 避免嵌套,利用return立刻返回的特点,减少else,让流程更线性,让代码更加易读。
  • 特殊情况先行,比如错误处理/continue/break的情况,先提前表达让其先行,剩下的主代码保持最小缩减,保持函数的逻辑清晰性。

小结:提高代码的阅读性,才能提高代码的可维护性和降低debug成本(故障大多出现在复杂的嵌套或循环语句中)。所以保持逻辑的线性原理,让其走直线,避免复杂嵌套,保持代码的逻辑简洁性,而非复杂。

  1. 错误和异常处理
  • 简单错误,仅出现一次的错误,且在其它地方不需要捕获该错误。优先使用error.New(string),如有格式要求,使用fmt.Errorf()
  • 错误的wrap和unwrap,包装返回的error,形成一个error跟踪链,采用fmt.Errorf(string,error),%w + err来将当前错误关联其中。
  • 错误判定,判定返回的错误是否为一个特定错误,使用error.Is(error,错误类型),比‘==’号的功能强大,可以判定错误链中是否包含指定错误。
  • 错误获取,从错误链中获取特定种类的错误,使用error.As(error,错误类型接收变量),可获取特定类型错误的全部信息。
  • panic,导致业务代码直接奔溃,如果该调用函数没有采用recover的话。能error则尽量error,必须panic终止程序,就panic,比如数据库都连不上,直接panic。一般在init和main中panic。
  • recover,只能在defer的函数中启用,嵌套无法生效,当前goroutine生效。在recover后可log记录当前调用栈,记录更多上下文信息。debug.Stack();err := fmt.Errorf("%v\n%s",recover的结果,debug.Stack());

小结:error尽可能提供简明的上下文信息,方便定位bug;panic用于异常;recover用于捕获panic,生效范围只在当前goroutine的被defer的函数中生效。

四、性能优化与实战

1、常见性能优化

  1. Benchmark,该工具可测函数单次运行时间,分配内存,分配次数等,go test -bench=. -benchmem
  2. slice,尽量使用make()初始化切片时提供的容量信息,扩容会浪费时间。注意多个slice可公用一个底层数组,所以尽量从copy,第一防止操作错误,第二方式小slice引用大array,内存得不到释放。
  3. Map,同理发生扩容带来的内容拷贝和rehash操作,可以预估一下需要的空间。
  4. strings.Builder,string是固定的,每次拼接会产生新字符串。同时可以用Grow(int)来分配底层byte数组的长度。和slice通用的扩容道理。
  5. 空结构体,节省内存,其不占任何内存空间,作为一个占位符。
  6. atomic包,atomic.AddInt32()这些方法比sync.Mutex加锁性能好的多。atomic是通过硬件实现,Mutex是操作系统实现,所以atomic效率高。Mutex应该将一段分散的逻辑变为原子逻辑,保护变量实在很亏。对于非数值操作,可以使用atomic.Value,能承载一个interface{}

小结:性能陷阱+内存陷阱,但普通的代码不要一味的追求程序的性能,越高级的优化手段越容易出现问题,也越复杂难理解,满足正确可靠、简洁清晰的质量要求的前提下来提高程序性能。

常见优化手段,

  • 串行改并行
  • 减少大量对象创建
  • 锁尽早释放,降低锁粒度
  • 同步调用改异步调用
  • JSON序列化使用第三方库代替系统库
  • sync.Pool进行堆对象分配的重用
  • 多个小对象合成1个大对象
  • 合并对象数,指针对象变成值对象(不需要gc去扫描了)
  • 善用空结构
  • 尝试降低gc频率以提高整体性能
  • unsafe转换例如:string和[]byte互相转

2、性能调优实战

  1. 原则,
  • 要依靠数据而不是猜测,环境和一些细节和我们想象的不一样。
  • 要定位最大瓶颈而不是细枝末节。
  • 不要过早优化,迭代快,各个阶段情况不一样,甚至有废掉的API。
  • 不要过度优化,越复杂越能出问题,而且不便于维护,而且迭代快,优化手段是否能够兼容。
  1. pprof分析工具
    什么地方耗费了多少CPU、Memory,pprof可以可视化这些数据。

注:pprof使用简介

总结

1)Go的包管理演进,以及Go mod的使用。
2)单元测试、Mock测试、基准测试。
3)常见性能优化

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;