> 文章列表 > Go工程基础知识

Go工程基础知识

Go工程基础知识

Go工程基础

  • 前言
  • 一、Go 包管理
    • 1、技术演进
    • 2、go mod
  • 二、Go测试
    • 1、单元测试
    • 2、测试框架
    • 3、基准测试
      • a.基本概念
      • b.编写基准测试函数
      • c.运行基准测试函数
      • d.基准测试样例演示
  • 三、编码规范
  • 四、性能优化与实战
    • 1、常见性能优化
    • 2、性能调优实战
  • 总结

前言

记录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环境变量,
变量名 作用 可选值 私有化仓库设置
GO111MODULE go 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 文件结构,
    Go工程基础知识
    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工程基础知识

二、Go测试

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

1、单元测试

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

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

通过“go test -v -run FuncName”命令来运行这些测试函数,该命令将自动查找和运行测试函数并生成测试报告
Go工程基础知识

  • 表驱动写法
    让你的测试代码更简洁,也可以让你更容易地添加新的测试用例。
    Go工程基础知识
    Go工程基础知识
  • 测试覆盖率
    对程序的测试程度一般通过测试覆盖率来衡量,通常指代码行数、分支数、函数数等统计数据中被测试用例覆盖到的比例。

在运行“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接口的类型,可以用来记录处理器写入响应的数据,并在测试中进行检查。

你可以使用它来创建一个响应记录器,并将其传递给你的处理器函数,然后从记录器中获取响应结果并与预期输出进行比较。
Go工程基础知识

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

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

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

Convey()函数用于描述测试的上下文,例如测试的功能或特性
So()函数用于描述测试的条件和结果,并在测试失败时提供详细的错误信息
Go工程基础知识

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

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

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

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

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

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

Go工程基础知识
Go工程基础知识
Go工程基础知识

Go工程基础知识

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

具体使用
ApplyFunc:为函数打桩。
ApplyMethod:为结构体成员方法打桩。
ApplyGlobalVar:为全局变量打桩
ApplyPrivateFunc:为私有函数打桩
ApplyPrivateMethod:为私有成员方法打桩
Go工程基础知识

3、基准测试

a.基本概念

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

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

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

b.编写基准测试函数

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

c.运行基准测试函数

运行基准测试需要使用go test命令,并加上-bench=表达式,用来匹配基准测试的函数。
Go工程基础知识
重要指标

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

d.基准测试样例演示

Go工程基础知识

三、编码规范

  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{}

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

2、性能调优实战

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

总结

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