高质量编程
高质量编程要求
要求
编程原则
- 简单性
- 消除”多余的复杂性“,以简单清晰的逻辑编写代码
- 难理解的代码无法修复改进
- 可读性
- 代码是写给人看的,而不是机器
- 编写可维护代码的第一步是确保代码可读
- 生产力
编码规范
代码格式
- gofmt
Go语言官方提供的工具,能自动格式化go语言代码为官方统一风格
常见IDE都支持方便的配置
- goimports
也是Go语言官方提供的工具,实际等于gofmt加上依赖包管理,自动增删依赖的包引用、将依赖包按字母序排序并分类
注释
- 解释代码作用:比如公共符号
- 解释代码如何做的:复杂代码的实现过程稍作解释
- 解释代码实现的原因:代码的外部因素,如外部输入;上下文,比如在前提xxx下执行该方法/代码
- 解释代码可能出错的情况:比如不正常输入会造成的异常,均写在注释中
命名规则
- 变量
- 简洁胜于冗长,在局部范围中,局部变量的命名尽可能简洁
- 缩略词全大写,如HTTP,但当其位于变量开头且不需要导出(不为外部引用)时,使用全小写
- 变量距离其被使用的地方越远,则需要携带越多的上下文信息,全局变量需要在名字中包含尽可能多的上下文信息
- 具有特定含义的变量名需要尽可能包含该包含的信息
- 函数
- 函数名不包含包名的上下文信息
- 函数名尽量短
- 当包内的函数返回类型与包名不同时,需要在函数名包含该返回类型的信息
- 包
- 小写字母组成,不包含大写/下划线
- 简短但包含一定的上下文信息
- 不能与标准库同名
- 不使用常用变量名作为包名,使用单数而非复数,谨慎使用缩写
控制流程
异常和错误处理
- 简单错误——仅出现一次的错误,且在其他地方不需要捕获该错误
- 优先使用errors.New来创建匿名变量直接表示简单错误
- 如果有格式化需求,使用fmt.Errorf
- 错误的Wrap和Unwrap——error之间存在嵌套关系,从而生成的error跟踪链
- 在fmt.Errorf中使用%w关键字来将一个错误关联至错误链中
- 错误判定——判定一个错误是否为特定错误
- 使用errors.Is(err,特定err)——返回bool,该方法可以判定错误链上的所有错误是否含有特定错误
- 使用==,只能判断当前返回的错误是否为特定错误
- 使用errors.As(err,特定err)——该方法可以获取错误链上的特定错误,通过特定错误对象显示错误信息
- panic和recover
性能调优
性能优化建议
性能优化的前提是满足正确可靠、简洁清晰等质量因素
性能优化是综合评估,有时候时间效率和空间效率可能对立
针对Go语言特性,介绍Go相关的性能优化建议
Benchmark
功能:为性能测试提供执行代码的单次消耗内存/花费时间等实际数据
指令go test -bench=. -benchmen
Slice
- 创建切片时,尽可能在使用make()初始化切片时提供容量信息。
data := make([]int,0,size)优于data := make([]int,0)
- 理由:Slice预分配内存
- 一个切片的创建,包含三个信息:数组指针,片段长度,片段容量。
- 向一个没有指定容量信息的切片中填充数据会经过两个步骤:扩容+填充,而拥有初始容量的切片,只需要填充。以上两种在容量不足前都会进行扩容。
- 使用已有切片的部分切片信息
- 在原有切片上切片——不会创建新的底层数组,将直接引用原底层数组,原底层数组的内存得不到释放,所需的内存较大
- 创建一个新的切片,将所需要的切片信息copy下来——创建新的底层数组,所占内存较小
Map
- 创建map时,尽可能在使用make()初始化map时提供容量信息。
data := make(map[int]int)优于data := make(map[int]int,size)
- 理由:Map预分配内存
- 不断向map中添加元素的操作会触发map的扩容
- 提前分配好空间可以减少内存拷贝和Rehash的消耗
- 建议根据实际需求提前预估好需要的空间
- Map的其他使用场景
实现set,可以考虑用map实现,此时,仅需要用到map的键(key),而不需要值。因为即便值设为bool类型(所占内存较小),也会占用一字节内存
字符串处理
常见字符串拼接方式
- string“+”string
s := ""
for i := range "abcd"{
s += i
}
- strings.Builder
var builder strings.Builder
var str = "hello"
for i := 0; i<5;i++{
builder.WriteString(str)
}
return builder.String()
- bytes.Buffer
buf := new(bytes.Buffer)
var str = "hello"
for i := 0; i<5;i++{
buf.WriteString(str)
}
return buf.String()
使用比较
- 使用“+”拼接性能最差,strings.Builder和bytes.Buffer差不多,strings.Builder更快
- 原因:
空结构体
- 空结构体struct{}示例不占据任何的内存空间
- 可作为各种场景下的占位符使用
- 好处:节省资源
- 好处:空结构体本身具备很强寓意,不需要任何值,仅作为占位符
func Fun1(n int) {
m := make(map[int]struct{})
for i := 0; i < n; i++ {
m[i] = struct{}{}
}
}
func Fun2(n int) {
m := make(map[int]bool)
for i := 0; i < n; i++ {
m[i] = false
}
}
atomic包
- 如何使用atomic包——保证系统并发安全运行
//使用atomic包
func AtomicAddOne(c *atomicCounter) {
atomic.AddInt32(&c.i, 1)
}
//加锁
func MutexAddOne(c *mutexCounter) {
c.m.Lock()
c.i++
c.m.Unlock()
}
- 二者比较
锁的实现是通过操作系统来实现的,属于系统调用,而atomic 操作是通过硬件实现,效率比锁高
锁——sync.Mutex 应该用来保护一段逻辑,不仅仅用于保护一个变量
对于非数值操作,可以使用 atomic.Value,能承载一个interface{}
性能调优原则
性能调优工具
pprof使用实战
- pprof功能简介
- pprof实战——项目搭建
搜索[https://github.com/wolforge/go-pprof-practice](https://github.com/wolforge/go-pprof-practice)下载项目
该项目中存在一些影响性能的问题代码,运行
- pprof实战——浏览器查看指标
浏览输入[http://localhost:6060/debug/pprof](http://localhost:6060/debug/pprof)
点击即可查看相关指标信息
pprof实战——查看CPU使用情况
打开任务管理器查看**go-pprof-practice **这个程序占用的CPU运行情况
在终端cmd,输入go tool pprof "http://localhost:6060/debug/pprof/profile?second=10"
- pprof实战——pprof工具的使用——top
- 从终端进入pprof工具后,可以输入
top命令,查看占用资源最多的函数
- 从终端进入pprof工具后,可以输入
- 分析五个参数指标
- 通常情况,当函数仅自运行,不调用其他函数时,其对应指标
flat==cum会等同于flat%==cum% - 当函数(func1)运行时会调用其他函数如(func2),则其
cum==flat(func1)+flat(func2),cum%==flat%(func1)+flat%(func2) - 但
flat==0说明函数中只有其他函数的调用,即该函数没有自己特有的代码部分,仅调用其他函数
- 分析:通常占用资源最多的函数可能就是性能瓶颈
- pprof实战——pprof工具的使用——其他命令
pprof实战——查看内存使用情况
打开任务管理器查看**go-pprof-practice **这个程序占用的内存使用情况
在终端cmd,输入go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
将打开如下页面,显示每个函数占用内存
- 点击VIEW即可切换视图
- top视图——按函数分析内存占用情况
- source视图——逐行分析占用内存
- 点击SAMPLE可以显示inuse的采样信息
pprof实战——查看goroutine情况
在终端cmd,输入go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"
- 点击VIEW,选择Flame Graph (火焰图)
- 火焰图是动态的,支持点击块进行分析
- 由上到下表示调用顺序,上调用下
- 每一个块代表一个函数,越长代表占用CPU的时间更长
- 点击View,选择Source
pprof实战——查看锁mutex的使用情况
在终端cmd,输入go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"
点击View,选择Source,查看哪里耗时较多,是否存在锁
pprof实战——查看阻塞block的使用情况
- 操作
在终端cmd,输入go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"
即可显示阻塞信息
- 问题
- 有的时候阻塞有很多,但并不会一一展示出来,为什么?
原因在于:在复杂的调用关系中,把所有的函数调用节点展示出来其实不利于定位问题,所以,系统设置了一些过滤措施,把占比较小的函数调用节点给省去,不显示。
- 如何显示被系统drop的阻塞节点呢?
浏览输入[http://localhost:6060/debug/pprof](http://localhost:6060/debug/pprof),在首页点击block查看所有阻塞的信息
pprof采样过程和原理
- CPU
- Heap堆
- goroutine
- block&mutex
性能调优案例
基本概念介绍
- 服务:能单独部署,承载一定功能的程序
- 依赖:如上图ServiceA的功能实现依赖ServiceB的响应结果
- 调用链路:能支持一个接口请求的相关服务集合及其相互之间的依赖关系
- 基础库:公共的工具包、中间件
业务服务优化流程
建立服务性能评估手段
- 服务性能评估方式
- 单独benchmark 无法满足复杂逻辑分析
- 不同负载情况下性能表现差异
- 请求流量构造
- 不同请求参数覆盖逻辑不同
- 线上真实流量情况
- 压测范围
- 单机器压测
- 集群压测
- 性能数据采集
分析性能数据,定位性能瓶颈
重点优化项改造
优化效果验证
进一步优化,服务整体链路分析
基础库调优与Go语言优化
AB实验SDK的优化
- 分析基础库核心逻辑和性能瓶颈
- 设计完善改造方案
- 数据按需获取
- 数据序列化协议优化
编译器&运行时优化
- 优化内存分配策略
- 优化代码编译流程
- 内部测压验证
- 推广业务服务落地验证
优点
总结
性能调优涉及到的方面有很多,有时优化了一方面,但另一方面却受到了限制,因此需要综合考虑,权衡利弊,一个完整的优化方案应考虑到更多的问题,不能为了解决一个难题创造更多难题。