这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天
1、高质量编程
1.1 简介
什么是高质量——编写的代码能够达到正确可靠、简介清晰的目标
- 边界条件
- 异常处理
- 易读易维护
编码原则:
- 简单性
- 消除“多余的复杂性”
- 不理解的代码无法修复改进
- 可读性
- 编写可维护的代码的第一步是确保代码可读
- 生产力
- 团队整体工作效率
1.2 编码规范
- 代码格式
- 注释
- 命名规范
- 控制流程
- 错误和异常处理
1.2.1 代码格式
- gofmt
Go语言官方的工具,能够自动格式化 Go 代码。Goland 中有相关配置
- goimports
Go语言官方的工具,等于gofmt加上依赖包管理。自动增删依赖的包引用、将依赖包按照字母序排序并分类
1.2.2 注释
- 简介
- 注释应该解释代码作用
-
- 适合注释公共符号
- 注释应该解释代码如何做
-
- 注释代码实现过程
- 注释应该解释代码实现的原因
-
- 解释代码外部原因
- 提供额外的上下文
- 注释应该解释代码什么情况会出错
-
- 解释代码的限制条件
- 公共符号始终要注释
- 公共符号都要有注释说明
- LimitedReader.Read 本身没有注释,但是它紧跟的 LimitedReader 结构声明,明确了它的作用
注意:不要注释实现接口的方法。
1.2.3 命名规范
- 变量
- 简洁胜于冗长
- 缩略词全大写,当其不需要导出的时候,可以使用权小写
-
- 例如:ServerHTTP 而不是 ServerHttp
- 使用 XMLHTTPRequest 或者 xmlHTTPRequest
- 变量距离其被使用的地方越远就需要携带更多的上下文信息
-
- 全局变量需要在名字中带更多的上下文信息
- 函数
- 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现
- 尽量简短
- 当名为 foo 的包某个函数的返回类型为 Foo 时,可以省略类型信息而不产生歧义
- 当名为 foo 的包某个函数的返回类型为 T(T 不是 Foo) 时,可以在函数名中加入类型信息
- 包
- 只能由小写字母组成
- 简短并包含一定上下文信息,例如 schema、task 等
- 不与标准库同名。不要使用 sync 或者 strings
尽量满足:
- 不适用常用变量作为包名。例如:使用 bufio 而不是 buf
- 使用单数而不是复数。例如:使用 encoding 而不是 encodings
- 谨慎使用缩写
1.2.4 控制流程
- 避免嵌套,保证正常流程清晰
- 尽量保证正常代码路径为最小缩进
- 优先处理错误情况/特殊情况,尽早返回或者继续循环减少嵌套
1.2.5 错误和异常处理
- 简单错误
- 指的是仅仅出现一次的错误,其他地方不需要捕获
- 优先使用 errors.New 来创建匿名变量来直接表示简单错误
- 如果有格式化的需求,使用 fmt.Errorf
- 复杂错误(错误的 Wrap 和 Unwrap)
- 错误的 Wrap 实际上是提供了一个 error 嵌套另一个 error 的能力,从而生成一个 error 跟踪链
- 在 fmt.Errorf 中使用:%w 关键字来将一个错误关联至错误链中
- 错误判定
- 判定一个错误是否为特定错误,使用 errors.Is
- 与 == 不同,该方法可以判定错误链上所有错误是否包含特定的错误
- 在错误链上获取特定种类的错误,使用 errors.As
- panic
- 不建议在业务代码中使用 panic
- 调用函数不包含 recover 会造成程序崩溃
- 若问题可以屏蔽或者解决,建议使用 error
- 程序启动阶段发生不可逆转的错误的时候,可以在 init 或者 main 函数中使用 panic
- recover
- recover 只能被 defer 函数中使用
- 嵌套无法生效
- 只能在当前的 goroutine 中生效
- defer 语句是后进先出的
- 如果需要更多上下文信息,可以 recover 后在 log 中记录当前的调用栈
1.3 性能优化建议
1.3.1 Benchmark
- 如何使用
- 性能表现需要实际数据衡量
- Go 语言提供了支持基准性能测试的 benchmark 工具
go test -bench=. -benchmem
- 结果说明
上述结果中第四项表示:每次执行申请多大内存
1.3.2 Slice
- slice 预分配内存
- 尽可能在使用 make() 初始化切片时 提供容量信息(如下图:建议右侧写法)
性能优化原因:
- 切片本质是是一个数组片段的描述
-
- 包括数组指针
- 片段长度
- 片段容量(不改变内存分配情况下的最大长度)
- 切片操作不复制切片指向的元素
- 创建一个新的切片会复用原来切片的底层数组
性能优化建议:
- 在已有切片基础上创建切片,不会创建新的底层数组
- 因此可以使用 copy 代替 re-slice
1.3.3 map 预分配内存
- 不断向 map 中添加元素会触发 map 扩容
- 提前分配好空间可以减少内存拷贝和 rehash 的消耗
1.3.4 使用 strings.Builder
常见字符串拼接方式:Plus(+)、StrBuilder、ByteBuffer
- 使用 + 拼接性能最差,strings.Builder,bytes.Buffer相近,strings.Builder更快
- 分析
-
- 字符串在 Go 中是不可变类型,占用内存大小是固定的
- 每次 + 都会重新分配内存
- strings.Builder 和 bytes.Buffer 底层都是 []byte 数组
- 内存扩容策略,不需要每次拼接和从新分配内存
- bytes.Buffer 转化为字符串时重新申请了一块空间
- strings.Builder 直接将底层的 []byte 转换成了字符串类型返回
进一步提升性能方法:
使用 Grow 方法预分配内存空间长度:builder.Grow(n * len(str));
1.3.5 空结构体
- 使用空结构体节省内存
- 空结构体 struct{} 实例不占据任何内存空间
- 可作为任何场景下的占位符使用
-
- 节省资源
- 本身具备很强的语义,即这里不需要任何值,仅仅作为占位符
注意:实现 Set 可以考虑 map 来代替。对于该场景,只需要使用 map 的键而不用 map 的值
1.3.6 atomic 包
atomic 和 加锁 的对比
- 锁 的实现是通过 系统调用
- atomic 操作时通过硬件实现,效率比锁高
- sync.Mutex 应该用来保护一段逻辑,不仅仅用于保护一个变量
- 对于非数值操作,可以使用 atomic.Value,能承载一个 interface{}
2、性能调优实战
2.1 简介
- 依靠数据不是猜测
- 不要过早优化 和 过度优化
2.2 性能分析工具 pprof
2.2.1 pprof 工具组成
2.2.2 排查实战
浏览器查看指标
CPU
- 命令:topN(查看占用资源最多的函数)
什么情况下 flat == cum?什么情况下 flat==0?
flat==cum:函数中没有调用其他函数
flat==0:函数中只调用了其他函数没有其他操作
- 命令:list(根据指定的正则表达式查找代码行)
- 命令:web(调用关系可视化)
Heap-堆内存
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
Top视图 与 Source视图
SAMPLE列表下:
- alloc_objects:程序累计申请的对象数
- inuse_objects:程序当前持有的对象数
- alloc_space:程序累计申请的内存大小
- inuse_space:程序当前占用的内存大小 goroutine-协程
goroutine泄露也会导致内存泄露
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"
切换到火焰图视角后:
- 由上到下表示调用顺序
- 每一块表示一个函数,越长表示占用CPU的时间越长
- 火焰图是动态的,支持点击块进行分析
- 支持搜索,可以在 Source 视图下搜索
mutex-锁
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"
与前面一样,先在graph视图里面看占用时间的方法,再在source视图里面看是哪一行代码有问题,将其注释
block-阻塞
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"
小结
2.2.3 采样过程和原理
- CPU:
- 采样对象:函数调用和它们占用的时间
- 采样率:100次/秒,固定值
- 采样时间:从手动启动到手动结束
- 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为每次加锁均记录
2.3 性能调优案例
2.3.1 业务服务优化
- 基本概念
- 服务:能单独部署,承载一定功能的程序
- 依赖:Service A 的功能实现依赖 Service B 的响应结果,称为 Service A 依赖 Service B
- 调用链路:能支持一个接口请求的相关服务集合及其相互之间的依赖关系
- 基础库:公共的工具包、中间件
- 流程
- 建立服务性能评估手段
-
- 服务性能评估方式
-
-
- 单独benchmark无法满足复杂逻辑分析
- 不同负载情况下性能表现差异
-
-
- 请求流量构造
-
-
- 不同请求参数覆盖逻辑不同
- 线上真实流量情况
-
-
- 压测范围
-
-
- 单机器压测
- 集群压测
-
-
- 性能数据采集
-
-
- 单机器性能数据
- 集群性能数据
-
- 分析性能数据,定位性能瓶颈
-
- 使用库不规范
- 高并发场景优化不足
- 重点优化项改造
-
- 正确性是基础
- 相应数据 diff
-
-
- 线上请求数据录制回放
- 新旧逻辑接口数据 diff
-
- 优化效果验证
-
- 重复压测验证
- 上线评估优化效果
-
-
- 关注服务键控
- 逐步放量
- 收集性能数据
-
- 进一步优化,服务整体链路分析
-
- 规范上游服务调用接口,明确场景需求
- 分析链路,通过业务流程优化提升服务性能
2.3.2 性能调优案例-基础库优化
- AB实验SDK的优化
-
- 分析基础库核心逻辑和性能瓶颈
-
-
- 设计完善改造方案
- 数据按需获取
- 数据序列化协议优化
-
- 内部压测验证
- 推广业务服务落地验证
2.3.3 Go 语言优化
- 编译器 & 运行时优化
-
- 优化内存分配策略
- 优化代码编译流程
- 内部压测验证
- 推广业务服务落地验证
通用性强,只需要调整编译配置
PS
本文主要作用是作为上课笔记,如有错误,欢迎大家评论指正,我会及时改正