Go语言工程实践|青训营笔记(三)
这是我参与「第五届青训营 」笔记创作活动的第3天
1.依赖管理
在项目开发中,任何项目免不了对第三方项目包依赖引用,引用第三方包可以方便直接调用想并获得结果,也可以避免代码逻辑的复杂和冗余,因此对依赖包的管理就尤为重要。Go语言的依赖管理发展主要经历了三个阶段,分别是GOPATH、Go Vendor、Go Module
1.1GOPATH
GOPATH可以看作是用户项目的工作目录,目录下主要有三个子目录
- bin:用于存储所编译生成的二进制文件。
- pkg:存储预编译的目标文件,以加快程序的后续编译速度。
- src:存储项目源码。
对于项目中所需要的包,GOPATH都会使用go get下载最新版本的包到src目录里。并且GOPATH模式下,一个第三方包只能有一个版本,所有本地的项目都只能用这一个版本的包 这样就会引发一些问题:
- 其他人运行你的开发的程序时,无法保证他下载的包版本是你所期望的版本,当对方使用了其他版本,有可能导致程序无法正常运行。
- 在本地一个包只能保留一个版本,意味着你在本地开发的所有项目,没法让不同项目对应不同的项目包版本,这几乎是不可能的。
1.2 Go Vendor
为了解决 GOPATH 方案下不同项目下无法使用多个版本库的问题,Go v1.5 开始支持Go Vendor。而Go Vendor则是在GOPATH目录中多增加了vendor目录,vendor目录用于存放项目所有的依赖包。每个项目需要的依赖包会直接下到项目的vendor目录里,项目之间的依赖包互不影响。
虽然这个方案解决了 GOPATH模式下的一些问题,但是方案并不完美:
1、如果多个项目用到了同一个包的同一个版本,这个包会存在于本地的不同目录下,不仅对磁盘空间是一种浪费,还无法对第三方包进行集中式的管理。
2、如果要分享你的项目,需要将所有的依赖包悉数上传;在别人使用的时候,除了项目的源码外,还要把所有的依赖包全部下载下来,才能保证别人使用的时候保证版本一致而正常运行。
1.3 Go Module
Go Module是 Golang 1.11 版本引入的官方包依赖管理工具,用于解决没有记录依赖包具体版本的问题,更方便依赖包的管理。与GOPATH、Go Vendor使用目录管理依赖包不同,Go Module使用go.mod文件来管理依赖包版本。依赖包可以下载到任意目录下,而项目目录下存在 go.mod文件,用来声明包的依赖关系。
如下为一个项目中的go.mod文件,可以看到go.mod文件中主要包含三部分。首先是模块路径,指示了找到该模块的路径信息,该模块前缀是github则表示该模块可以在GitHub上找到,并且模块的源代码由github托管管理。接着是模块的原生SDK版本。最后则是单元依赖,每个依赖单元都由模块路径和版本号组成,这样就可以区分同一依赖包的不同版本。单元依赖的后缀"indirect"则表示该模块属于间接依赖,当前模块并没有直接导入该模块的包
module github.com/xxxx/go-project-example
go 1.16
require (
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.3.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/ugorji/go v1.2.7 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.21.0
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f // indirect
google.golang.org/protobuf v1.28.0 // indirect
)
Go Module中可以使用go mod命令来对依赖进行下载和管理,其主要包含下面几个指令:
- go mod download:下载模块到本地缓存
- go mod init:初始化当前项目,创建 go.mod 文件
- go mod tidy:增加缺少的 module,删除没有用的 module
在实际项目开发工程中,go.mod文件不要手动修改,最好使用go mod指令更新go.mod文件。
2.单元测试
为了避免一些事故发生,在上线部署之前都会对项目进行测试,而测试可以分为三种测试:
- 回归测试:通过终端回归一些固定的主流程的场景。
- 集成测试:对系统功能维度进行验证。
- 单元测试:主要在开发阶段进行测试,对单独的函数、模块进行功能测试。
从上至下,测试成本逐渐降低,测试覆盖率却逐步上升,所以单元测试一定程度上决定了代码的质量。
在Go语言中,单元测试需要与测试的函数在同一目录下,命名以_test.go结尾,测试文件需要用到testing包,测试函数以func TestXxx(t *testing.T)。如下为一个单元测试的实例,功能代码需要输出"Tom"字符串,而测试代码就是判断函数能不能正确输出"Tom"。
//功能代码
func Hello() string {
//return "Tom"
return "Jerry"
}
//测试代码
package test
import (
"testing"
)
func TestHello(t *testing.T) {
output := Hello()
expectoutput := "Tom"
if output != expectoutput {
t.Errorf("error output")
}
}
当返回值为"Tom",可以看到测试结果如下图,PASS通过。
而将返回值改为"Jerry"后,测试结果就失败了,并且提示错误信息“Expected output Tom do not match Jerry”
3.基准测试
除了验证函数功能的实现,在实际项目开发中,为了对优化代码,往往还需要对代码的性能做测试,这就用到了基准测试,基准测试的方法类似单元测试。
基准测试以Benchmark开头,入参为testing.B。下面是一个基准测试的实例,测试的函数是随机选择数组中的索引并返回对应的值。测试部分主要是对函数的耗时进行测试。
//待测试函数
import "math/rand"
var Severindex [10]int
func InitSeverIndex() {
for i := 0; i < 10; i++ {
Severindex[i] += 100
}
}
func Slect() int {
return Severindex[rand.Intn(10)]
}
//测试代码
func BenchmarkSlect(b *testing.B) {
InitSeverIndex()
b.ResetTimer()
for i := 0; i < b.N; i++ {
Slect()
}
}
func BenchmarkSlectParallel(b *testing.B) {
InitSeverIndex()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Slect()
}
})
}
测试函数中的b.N表示一个计数器,测试一个用例的默认测试时间是1秒,当测试用例函数不到1秒时,b.N就按1,2,5,10,20,50...递增,并以递增后的值重新测试。
在测试函数内部,我们应该先进行一些初始化操作以及一些不在基准测试范围内的工作,之后再重置计时器。
测试结果如下,可以看到该每执行一次用例函数耗时13.58ns。
RunParallel表示多协程并发测试,最终结果如下,可以发现并发情况下代码性能变差,主要原因是随机数函数rand为了保证全局的随机性和并发安全,加入了全局锁,从而导致耗时增加。
而为了解决这一随机性能问题,这里使用了一个开源的随机数方法(地址:bytedance/gopkg: Universal Utilities for Go (github.com)),这里再进行基准测试,可以看到结果提升了近百倍。