这是我参与「第五届青训营 」伴学笔记创作活动的第5天。
依赖管理
Go语言的依赖管理的进化历程目前共经历了三个阶段:
GOPATH -> GO Vendor -> Go Module
由于不同环境(项目)依赖的版本不同,因此我们需要一个类似于Java的npm、Python的pip工具来帮助我们能够顺利获取病使用项目中所需的各个模块的功能,并不断进行迭代。
GOPATH
GOPATH是Go语言支持的环境变量,是工作区。
在GOPATH中:
bin:存放项目编译生成的二进制文件;
pkg:项目编译的中间产物,用于加速编译;
src:项目源码,项目代码依赖于此。
在GOPATH这个阶段,通过go get 命令就可以下载最新版本的包到src目录下。
弊端
对于不同工程的可能依赖的pkg版本不同,而GOPATH无法实现package的多版本控制。
GO Vendor
基于GOPATH的弊端,在GO Vendor时期GO项目目录下增加了vendor文件,存放当前项目依赖包,以副本形式放在 $ProjectRoot/vendor,自此在编译器在找包时会优先在该目录下找,如果没有找到,则会再返回GOPATH寻找。
弊端
由于不同工程的依赖的pkg版本不同,出现依赖冲突,导致编译错误,不能清晰的标识依赖某一个版本。
GO Module
基于能使各模块依赖的包版本问题,以及在包管理上达到去中心化的目的(与之设计理念相反的如Python的pip),达到定义版本规则和管理项目依赖关系的目标,Go Module应运而生,也就是人们常说的go mod,并成为Go语言官方的依赖管理工具。
其重要构成部分有:
go.mod文件:管理依赖包版本,配置文件、描述依赖;
go get:依赖包获取指令,通过go get可直接以输入URL的方式获取托管到平台上的仓库中的模块,这也是去中心化的好处之一;
proxy:中心仓库管理依赖库,是一个依赖站点,保证依赖的稳定性,出现问题可以考虑使用多个proxy。
(建议刚开始使用go mod管理项目的朋友将go环境中的GOPROXY设置为goproxy.cn,direct ,这个站点是国内的,以防下载不了所需的包)
性能优化
首先要明确一个原则:
性能的优化与瓶颈的定位是要基于数据的,而不是猜测;要定位最大瓶颈而不是细枝末节。
提升性能首先要建立服务性能评估手段,选择合适的工具;
分析性能数据、定位性能瓶颈;
基于正确性重点优化改造项;
通过压测等各种测试手段基于大样本来进行优化效果的验证,并再次收集性能数据。
工具篇
benchmark
go语言提供了支持基准性能测试的benchmark工具:
go test -bench=. -benchmem
该工具能支持对某一函数的性能进行数以百万计次的测试,返回其平均需要的执行时间、申请多大的内存、申请几次内存的测试结果。
pprof
pprof是go自带的用于可视化和分析性能分析数据的工具,比如可以得知在什么地方耗费了多少Cpu、Memory,可以精确到具体哪一行代码。
通过:
go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10" (seconds后的参数为可设定的采样时间)指令并在游览器中访问其端口来查看性能报告。
其所能观测的指标有:
CPU
以下介绍相关指令与参数所表征的含义,在后续模块中就不进行详细介绍了。
topN --> 查看占用资源最多的函数
flat : 当前函数本身的执行耗时
flat% : flat占CPU总时间的比例
sum% : 上面每一行的%flat综合
cum : 指当前函数本身加上其调用函数的总耗时
cum% : cum占CPU总时间的比例
若flat == cum:
函数中没有调用其他函数
若flat == 0:
函数中只有其他函数的调用
再通过 list 函数名 就可以获取具体语句的资源消耗情况啦。
采样对象:函数调用和他们占用的时间
采样率:100次/秒,固定值
采样时间:从手动启动到手动结束
堆内存(heap)
通过go tool pprof -http=:8080 http://localhost:6060/debug/pprof/heap 并访问相关端口查看。
其采样原理是:
采样程序通过内存分配器在堆上分配和释放的内存,记录分配/释放的大小和数量;
采样率:每分配512KB记录一次,可在运行开头修改
goroutine
访问方式与堆内存基本相同,将URL的结尾改为goroutine即可,goroutine的查看的是基于可视化的火焰图的,其中:
- 由上到下表示调用顺序
- 每一块代表一个函数,越长代表占用CPU的时间越长
- 火焰图是动态的,支持点击块进行分析
pprof还可以查看mutex、block、mutex的使用情况,并以 TOP、调用图、火焰图、Peek、源码、反汇编的形式展示,大家可以根据自己的性能分析需求来对于不同的指标并以不同的方式定位。
语言篇
对于slice的优化建议
预分配内存
尽可能在使用make()初始化切片时提供容量信息
原因:不预分配会有内存拷贝的过程,而扩容是比较耗费时间的,其会有更多次的内存调用。
func NoPreAlloc(size int) {
data := make([]int, 0)
for k := 0; k < size; k++ {
data = append(data, k)
}
}
func PreAlloc(size int) {
data := make([]int, 0, size)
for k := 0; k < size; k++ {
data = append(data, k)
}
} //后者会显著更快
大内存未释放
性能较低原因:在已有切片基础上创建切片,不会创建新的底层数组。
场景:
原切片较大,代码在原切片基础上新建小切片
原底层数组在内存中有引用,得不到释放
优化建议:可使用copy代替re-slice
func GetLastBySlice(origin []int) []int {
return origin[len(origin)-2:]
}
func GetLastByCopy(origin []int) []int {
result := make([]int, 2)
copy(result, origin[len(origin)-2:])
return result
}
对于map的优化建议
预分配内存
map与slice同为引用类型,预分配内存会是内存分配的次数更少,创建速度更快。
分析:
- 不断向map中添加元素的操作会触发map的扩容
- 提前分配好空间可以减少内存拷贝和Rehash的消耗
- 根据实际需求提前预估好需要的空间
对于字符串拼接的优化建议
字符串拼接常见的有:
// 使用+拼接
func Plus(n int, str string) string {
s := ""
for i := 0; i < n; i++ {
s += str
}
return s
}
// 使用strings.Bulider或bytes.Buffer
// 两者的区别在于
// bytes.Buffer:转化为字符串时重新申请了一块空间
// strings.Builder:直接将底层的[]byte转换成了字符串类型返回
func StrBuilder(n int, str string) string {
var builder strings.Builder
for i := 0; i < n; i++ {
builder.WriteString(str)
}
return builder.String()
}
通过benchmark测试,结果显示:使用+拼接性能最差,strings.Builder、bytes.Buffer相近,string.Buffer更快。
分析:
- 字符串在go语言中是不可变类型,占用内存大小是固定的
- **使用+每次都会重新分配内存 **
- strings.Builder和bytes.Buffer底层都是[]byte
使用预分配的话性能也会有提升哦,当然这适用于已经能提前知道或预估bulider容量的情况。
func PreStrBuilder(n int, str string) string {
var builder strings.Builder
builder.Grow(n * len(str)) // 类似于slice的提前初始化
for i := 0; i < n; i++ {
builder.WriteString(str)
}
return builder.String()
}
对于空结构体的优化建议
使用空结构体可以节省内存。
空结构体struct{}实例不占据任何的内存空间,可作为各种场景下的占位符使用。
其主要作用为:
1.节省资源
2.空结构体本身具备很强的语义,即这里不需要任何值,仅作为占位符
func EmptyStructMap(n int) {
m := make(map[int]struct{})
for i := 0; i < n; i++ {
m[i] = struct{}{}
}
// 和存储布尔类型的数据的性能几乎一致
}
func BoolMap(n int) {
m := make(map[int]bool)
for i := 0; i < n; i++ {
m[i] = false
}
}
应用:实现Set,可以考虑使用map来实现,使用空结构体作为值。
以上内容若有不正之处,恳请您不吝指正!