这是我参与「第三届青训营 -后端场」笔记创作活动的的第3篇笔记。
高质量编程
高质量代码的标准
正确可靠、简洁清晰
- 考虑完备边界条件
- 异常情况处理
- 易读易维护
编码规范
目标是降低阅读理解代码的成本。
重点是考虑上下文信息,设计简洁清晰的名称。
代码格式
推荐使用gofmt自动格式化代码。
goimports还能管理依赖包。
注释
- 解释代码作用
- 解释代码如何做的
- 解释代码实现的原因
- 解释代码什么情况会出错
包中公共的部分必须要注释。
例外:不需要注释实现接口的方法。
代码是最好的注释。
注释应该提供代码未表达出的上下文信息。
命名规范
命名最好带有更多有效的信息。
- 简洁胜于冗长
- 缩略词全大写 、变量开头不需要公开就全小写
- 变量距离被使用的地方越远,就需要携带更多的上下文信息。比如全局变量。
函数命名规范
- 函数不携带包名的上下文信息,因为包名与函数名总是成对出现。
- 函数名尽量简短。
- 当名为foo的包某个函数返回类型Foo时,可以省略类型信息而不导致歧义。
- 当名为foo的包某个函数返回类型T(不是Foo)时,可以在函数名内加入类型信息。
包命名规范
- 只由小写字母组成。
- 简短并包含一定上下文信息。
- 不要与标准库同名。
- 不要使用常用变量名。
- 使用单数。
- 谨慎的使用缩写。
控制流程
- 避免嵌套。
- 优先处理错误情况、特殊情况,尽早返回或继续循环来减少嵌套。
- 线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支。
- 正常流程代码沿着屏幕向下移动。
- 提升代码可维护性和可读性。
- 故障问题大多出现在复杂的条件语句和循环语句中。
错误和异常处理
error提供简明的上下文信息链,方便定位问题。panic用于真正异常的情况。
简单错误
只出现一次,在其他地方不需要捕获该错误。
errors.New创建匿名变量来表示简单错误。
格式化需求用fmt.Errorf
错误的Wrap和Unwrap
错误的Wrap实际上提供了一个error嵌套另一个error的能力,从而生成一个error的跟踪链。
在fmt.Errorf中使用%w关键字来将一个错误关联至错误链中。
错误判断
errors.Is判断错误是否是指定类型的错误,可以判定错误链上所有错误是否含有特定的错误。
errors.As在错误链上获取特定种类的错误。
panic
- 不建议在业务代码中使用
panic - 调用函数不包含
recover会导致程序崩溃 - 若问题可以被屏蔽或解决,建议使用
error代替panic - 当程序启动阶段发生不可逆转的错误时,可以在
init或main函数中使用panic
recover
recover只能在被defer的函数中使用。- 函数嵌套中
recover无法生效。 - 只在当前
goroutine生效。 defer的语句是后进先出。- 如果需要更多的上下文信息,可以
recover后在log中记录当前的调用栈debug.Stack()。
性能优化建议
- 性能优化的前提是满足正确可靠、简介清晰等质量因素。
- 性能优化是综合评估,有时候时间效率与空间效率可能对立。
避免常见的性能陷阱可以保证大部分程序的性能。
普通应用代码,不要一味地追求程序的性能。
越高级的性能优化手段越容易出现问题。
在满足正确可靠、简洁清晰的质量要求的前提下提高程序性能。
Benchmark
- 性能表现需要实际数据衡量。
go test -bench=. -benchmem命令
ns/op每次执行花费多少纳秒。
B/op每次执行申请多大的内存。
allocs/op每次执行申请几次内存。
slice
预分配内存
- slice预分配内存。
- 切片操作并不复制切片指向的元素。
- 在已有切片上创建一个新的切片会复用原来切片的底层数组。
大内存未释放
在大切片上新建小切片会导致原底层数组的内存中有引用,得不到释放。
可以使用copy代替切片操作。
map
预分配内存
- map预分配内存。
- 提前分配空间减少内存拷贝与
rehash的消耗。
字符串处理
使用strings.Builder
Grow()同样也可以预分配内存。
使用strings.Builder拼接字符串,底层是byte数组。
strings.Builder优于bytes.Buffer优于+
bytes.Buffer转化为字符串时重新申请了一块空间。
strings.Builder直接将底层的byte数组转换成了字符串类型返回。
+拼接字符串每次都会重新分配内存。
空结构体
空结构体实例不占据任何内存空间,可以作为占位符,节省资源。
可以通过map空结构体实现set,只需要用到map的键。
atomic包
- 锁的实现是通过操作系统来实现,属于系统调用。
atomic操作是通过硬件实现,效率比锁高。sync.Mutex应该用来保护一段逻辑,不仅仅用于保护一个变量。- 对于非数值操作,可以使用
atomic.Value,能承载一个interface{}
性能调优实战
性能调优原则
- 要依靠数据不是猜测。
- 要定位最大瓶颈而不是细枝末节。
- 不要过早优化。
- 不要过度优化。
性能分析工具 pprof
go tool pprof [pprof网页URL CPU命令]
top查看占用资源最多的函数。
CPU
flat当前函数本身的执行耗时flat%flat占CPU总时间的比例sum%上面每一行的flat% 总和cum指当前函数加上其调用函数的总耗时cum%cum占CPU总时间的比例
flat==cum函数中没有调用其他函数。
flat==0函数中只有其他函数的调用。
list [正则表达式]查找代码行。
web生成可视化调用关系。
Heap堆内存
go tool pprof [网页URL heap命令]
alloc_objects程序累计申请的对像数。alloc_space程序累计申请的内存大小。inuse_objects程序当前持有的对像数。inuse_space程序当前占用的内存大小。
goroutine协程
go tool pprof [网页URL goroutine命令]
火焰图
mutex锁
go tool pprof [网页URL mutex命令]
block阻塞
go tool pprof [网页URL bolck命令]
pprof采样过程和原理
CPU
- 采样对象:函数调用和它们占用的时间
- 采样率:100次/秒,固定值
- 采样时间:从手动启动到手动结束
Heap堆内存
- 采样程序通过内存分配器在堆上分配和释放的内存,记录分配/释放的大小和数量
- 采样率:每分配512KB记录一次,可在运行开头修改,1位每次分配均记录
- 采样时间:从程序运行开始到采样时
- 采样指标:alloc_space,alloc_objects,inuse_space,inuse_objects
- 计算方式:inuse = alloc - free
goroutine协程&ThreadCreate线程创建
goroutine
- 记录所有用户发起且在运行的goroutine runtime.main的调用栈信息。
ThreadCreate
- 记录程序创建的所有系统线程的信息
Block阻塞&Mutex锁
Block
- 采样阻塞操作的次数和耗时
- 采样率:阻塞耗时超过阈值的才会被记录 1为每次阻塞均记录
锁
- 采样争抢锁的次数和耗时
- 采样率:只记录固定比例的锁操作,1位每次加锁均记录。
性能调优案例
业务服务优化
- 服务:能单独部署,承载一定功能的程序
- 依赖:A的功能依赖B的响应结果成为A依赖B
- 调用链路:能支持一个接口请求的相关服务集合及相互之间的依赖关系
- 基础库:公共的工具包、中间件
流程
-
建立服务性能评估手段
- 服务器性能评估方式
- 请求流量构造
- 压测范围
- 性能数据采集
-
分析性能数据,定位性能瓶颈
- 使用库不规范
- 高并发场景优化不足
-
重点优化项改造
- 正确性是基础,响应数据diff
- 线上请求数据录制回放
- 新旧逻辑接口数据diff
-
优化效果验证
-
重复压测验证
-
上线评估优化效果
- 关注服务监控
- 逐步放量
- 收集性能数据
-
进一步优化,服务整体链路分析
规范上游服务调用接口,明确场景需求
分析链路,通过业务流程优化提升服务性能
基础库优化
-
分析基础库核心逻辑和性能瓶颈
- 设计完整改造方案
- 数据按需获取
- 数据序列化协议优化
-
内部压测验证
-
推广业务服务落地验证
Go语言优化
- 优化内存分配策略
- 优化代码编译流程,生成更高效的程序
- 内部压测验证
- 推广业务服务落地验证
优点
- 接入简单,只需要调整编译配置
- 通用性强