- 这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天
1. 重点内容
- 编码规范
- 性能优化
2. 知识点
高质量编码
- 各种边界条件是否考虑完备
- 异常处理,稳定性保证
- 易读以维护
2.1 编码规范
-
代码格式
- 使用
gofmt自动格式化代码
- 使用
-
注释
-
注释应该解释代码的
-
作用:适合注释公共符号
// Open opens the named file for reading. func Open(name string) (*File, error) { return OpenFile(name, O_RDONLY, 0) } -
如何做的:适合注释实现过程
// Add the Referer header from the most recent request URL // to the new one, if it's not https -> http: if ref := refererForURL(reqs[len(reqs)-1].URL, req.URL); ref != "" { req.Header.Set("Referer", ref) } -
实现的原因:适合解释代码的外部因素,提供额外上下文
-
什么情况会出错:适合解释代码的限制条件
-
-
公共符号始终要注释
- 包中声明的每个公共的符号:变量、常量、函数和结构体
- 任何不明显也不简短的公共功能
- 库中的任何函数
- 例外:实现接口的方法不需要注释
- 若结构体注释已经说明它的函数的作用,则该函数不需要注释
-
-
命名规范
-
简洁胜于冗长
// bad for index := 0; index < len(s); index++ { // do something } // good for i := 0; i < len(s); i++ { // do something } -
缩略词全大写,当其位于变量的开头并不需要导出时,使用全小写
-
变量距离其被使用的地方越远,需要携带的上下文信息越多
-
函数名
- 不携带包名的上下文信息,因为包名和函数名是成对出现的
-
包名
- 只由小写字母组成,不包含大写字母和下划线
- 简短并包含一定上下文信息
- 不与标准库同名
- 尽量满足:不使用常用变量作为包名、使用单数、谨慎使用缩写
-
-
控制流程
-
避免嵌套,保持正常流程
// bad if foo { return x } else { return nil } //good if foo { return x } return nil -
尽量保持正常代码路径为最小缩进
// bad func OneFunc() error { err := doSomething() if err == nil { err := doAnotherThing() if err == nil { // normal case return nil } return err } return err } //good func One Func() error { if err := doSomething(); err != nil { return err } if err := doAnotherThing(); err != nil { return err } // normal case return nil }
-
-
错误和异常处理
-
简单错误
- 简单的错误指的是仅出现一次的错误,且在其他地方不需要捕获该错误
- 优先使用
errors.New来创建匿名变量来直接表示简单错误 - 如果有格式化的需求,使用
fmt.Errorf
-
错误的Wrap和Unwrap
- 错误的包装提供了一个error嵌套另一个error的能力,生成一个error的跟踪链
- 在
fmt.Errorf中使用:%w关键字来将一个错误wrap至其错误链中
-
错误判定
- 判定是否为特定错误,使用
errors.Is()。该方法可以判定错误链上的所有错误是否含有特定的错误 - 在错误链上获取指定种类的错误,使用
errors.As()
- 判定是否为特定错误,使用
-
panic和recover-
panic- 不建议在业务代码中使用
panic发生后,会向上传播至调用栈顶,如果当前goroutine中所有deferred函数都不包含recover就会造成整个程序崩溃- 建议使用error代替panic
- 当程序启动阶段发生不可逆转的错误时,可以在init或main函数中使用panic
-
recoverrecover只能被defer的函数使用- 嵌套无法生效
- 只在当前的
goroutine生效 defer的语句后进先出- 如果需要跟多上下文信息,可以
recover后在log中记录当前的调用栈
-
-
2.2 性能优化
在满足正确性、可靠性、健壮性、可读性等质量因素的前提下,设法提高程序的性能 有时候时间效率和空间效率可能对立,此时应当分析那个更重要,作出适当的折衷。 针对Go语言编程,介绍Go相关的性能优化建议
-
Benchmark-
Go语言提供了支持基准性能测试的
benchmark工具go test -bench=. -benchmen
-
-
slice-
预分配内存
-
大内存释放问题
- 原切片由大量的元素构成,在原切片的基础上切片,虽然只使用了很小一段,但底层数组在内存中仍然占据了大量空间,得不到释放
- 使用
copy替代re-slice
-
-
map- 预分配内存
-
字符串处理
- 使用
strings.Builder - 使用+拼接性能最差,
strings.Builderbtyes.Buffer相近,strings.Builder更快
- 使用
-
空结构体
- 空结构体实例不占据任何内存空间,作为占位符使用
-
atomic包- 保证计数准确,线程安全
2.3 性能优化工具
-
采样过程和原理
-
CPU
-
采样对象:函数调用和它们占用的时间
-
采样率:100次/秒,固定值
-
采样时间:从手动启动到手动结束
- 启动采样时,进程向OS注册一个定时器,OS会每隔10ms向进程发送一个SIGPROF信号, 进程接收到信号后就会对当前的调用栈进行记录。与此同时,进程会启动一个写缓冲的goroutine,它会每隔100ms从进程中读取已经记录的堆栈信息,并写入到输出流。
- 当采样停止时,进程向OS取消定时器,不再接收信号,写缓冲读取不到新的堆栈时,结束输出
-
-
Heap
- 采样程序通过内存分配器在堆上分配和释放的内存,记录分配/释放的大小和数量
- 采样率:默认每分配512KB内存会采样一次, 采样率是可以在运行开头调整的,设为1则为每次分配都会记录
- 采样时间:从程序运行到采样时
- 采样指标:
alloc_space,alloc_objects,inuse_space,inuse_objects - 计算方式:
inuse = alloc - free
-
Goroutine&ThreadCreate-
Goroutine- 记录所有用户发起且在运行中的
goroutine(即入口非runtime开头的)runtime.main的调用栈的信息
- 记录所有用户发起且在运行中的
-
ThreadCreate- 记录程序创建的所有系统线程的信息
-
-
Block & Mutex-
阻塞操作
- 采样阻塞操作的次数和耗时
- 采样率:阻塞耗时超过阈值的才会被记录,1为每次阻塞均记录
-
锁竞争
- 采样争抢锁的次数和耗时
- 采样率:只记录固定比例的锁操作,1为每次加锁均记录
-
-
-
性能调优案例
-
业务服务优化
-
基本概念
- 服务:能单独部署,承载一定功能的程序
- 依赖:Service A的功能实现依赖Service B的响应结果,成为Service A依赖Service B
- 调用链路:能支持一个接口请求的相关服务集合及其相互之间的依赖关系
- 基础库:公共的工具包,中间件
-
流程
-
建立性能评估手段
-
服务性能评估方式
- 单独
benchmark无法满足复杂逻辑分析 - 不同负载情况下性能表现差异
- 单独
-
请求流量构造
- 不同请求参数覆盖的逻辑不同
- 线上真实流量情况
-
压测范围
- 单机器压测
- 集群压测
-
性能数据采集
- 单机性能数据
- 集群性能数据
-
-
分析性能数据,定位性能瓶颈
-
使用库不规范
- 日志使用不规范:一部分是调试日志发布到线上,一部分是线上服务在不同的调用链路上数据有差别。到真实线上全量场景,这会导致日志量增加,影响性能
-
高并发场景优化不足
-
-
重点优化项改造
-
正确性是基础
- 在变动较大的性能优化上线之前,还需要进行正确性验证,因为线上的场景和流程太多,所以要借助自动化手段来保证优化后程序的正确性
-
响应数据
diff- 线上请求数据录制回放
- 新旧逻辑接口数据
diff
-
-
优化效果验证
-
重复压测验证
- 压测并不能保证和线上表现完全一致,有时还要通过线上的表现再进行分析改进,是个长期的过程
-
上线评估优化效果
- 关注服务监控
- 逐步放量
- 收集性能数据
-
-
进一步优化,服务整体链路分析
- 规范上游服务调用接口,明确场景需求
- 分析链路,通过业务流程优化提升服务性能
-
-
-
基础库优化
-
AB实验的SDK优化-
分析基础库核心逻辑和性能瓶颈
- 设计完善改造方案
- 数据按需获取
- 数据序列化协议优化
-
内部压测验证
-
推广业务服务落地验证
-
-
-
Go语言优化
-
编译器&运行时的优化
- 优化内存分配策略
- 优化代码编译流程,生成更高效的程序
- 内部压测验证
- 推广业务服务落地验证
优点:
- 接入简单,只需要调整编译配置
- 通用性强
-
-