这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天
高质量编程
-
编写的代码能够表达正确可靠、间接清晰的目标
- 各种边界条件是否考虑完备
- 异常情况处理、稳定性保证
- 易读易维护
-
编程原则
- 简单性
- 可读性
- 生产力
-
编码规范
-
代码格式
- 使用gofmt自动格式化代码,保证所有的Go代码与官方推荐格式保持一致
-
注释
-
命名规范
-
variable
- 简洁胜于冗长
- 缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
- 变量距离其被使用的地方越远,需要携带越多的上下文信息
- 全局变量在其名字中需要更多的上下文信息,是的在不同的地方可以轻易辨认
-
function
- 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的
- 函数名尽量简短
-
package
- 只有小写字母组成,不包含大写字母和下划线等字符
- 简短并包含一定的上下文信息
- 不要与标准库同名
关于命名大多数规范核心在于上下文
-
-
控制流程
- 避免嵌套,保持正常流程清晰
- 尽量保持正常代码路径的最小缩进,优先处理错误情况/特殊情况,并尽早返回或继续循环来减少嵌套,增加可读性
线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支,提高代码可读性
-
错误和异常处理
-
简单的错误处理
- 优先使用errors.New来创建匿名变量来直接表示错误,有格式化需求时使用fmt.Errorf
-
错误的Wrap何画Unwrap
- 在fmt.Errorf中使用%w关键字来将一个错误wrap至其错误链中
-
错误判定
- 使用errors.Is可以判定错误链上的所有错误是否含有特定的错误
- 在错误链上获取特定种类的错误,使用errors.As
-
panic
- 不建议在业务代码中使用panic
- 如果当前goroutine中所有deferred函数中不包含recover就会造成整个程序崩溃
- 当程序启动阶段发生不可逆转错误的时候,可以在init或main函数中使用panic
-
recover
- recover只能被defer的函数中使用,嵌套无法生效,只在当前goroutine生效
- 如果需要更多的上下文信息,可以recover后的log中记录当前的调用栈
panic用于真正异常的情况
error尽可能提供简明的上下文信息,方便定位错误
recover生效范围,在当前goroutine的被defer的函数中生效
-
-
性能调优实战
- Benchmark
go test -bench=. -benchmem
-
slice 预分配内存
-
原理
-
切片本质是一个数组片段的描述,包括了数组的指针,这个片段的长度和容量(不改变内存分配情况下的最长长度)
-
切片操作并不复制切片指向的元素,创建一个新的切片会复用原来切片的底层数组,因此切片操作时非常高效的
-
切片有三个属性,指针(ptr)、长度(len)和容量(cap)。append有两种场景:
- 当append之后的长度小于等于cap,将会直接利用员底层数组剩余的空间
- 当append之后的长度大于cap,则会分配一块更大的区域来容纳新的底层数组
因此,为了避免内存发生拷贝,如果能够知道最终的切片的大小,预先设置cap的值能够获得更好的性能
-
-
陷阱:大内存未释放
- 在已有切片基础上创建切片,不会创建新的底层数组
- 原切片较大,代码在原切片的基础上新建小切片
- 原底层数组在内存中应用,得不到释放
- 可以使用copy代替re-slice
-
-
map预分配内存
- 不断像map中添加元素的操作会触发map的扩容
- 根据实际情况提前估算好需要的时间
- 提前分配好空间可以减少内存拷贝和Rehash的消耗
-
字符串处理
-
使用strings.Builder
- 使用+拼接性能最差,strings.Builder, bytes.Buffer相近,strings.Builder更快
-
分析
- 字符串在Go语言中是不可变类型,占用内存大小是固定的
- 使用+每次都会重新分配内存
- strings.Builder, butes.Buffrer底层都是[]byte数组
- 内存扩容策略,不需要每次拼接重新分配内存
-
-
空结构体
-
空结构体节省内存
-
空结构体struct{}实例不占据任何内存空间
-
可以作为哥哥场景下占位符使用
- 节省资源
- 空结构体本身具备很强的语义,即这里不需要任何值,仅作为占位符
-
实现Set,可以考虑用map来代替
-
即使是将map的值设置为bool类型,也会多占据1个字节空间
-
-
-
atomic包
- 锁的实现是通过操作系统来实现,属于系统调用
- atomic操作时通过硬件实现,效率比锁高
- sync.Mytex应该用来保护一段逻辑,不仅仅用于保护一个变量
- 对于非数值操作,可以使用stomic.Value,能承载一个interface{}
性能调优原则
- 要依据数据而不是猜测
- 要定位最大瓶颈而不是细枝末节
- 不要过早优化
- 不要过度优化
pprof工具
-
工具 Tool
- runtime/pprof
- net/http/pprof
-
采样 Sample
- CPU
- 堆内存 Heap
- 协程 Goroutine
- 锁 Mutex
- 阻塞 Block
- 线程创建 ThreadCreate
-
展示 View
- Top
- 调用图 Graph
- 火焰图 FlameGraph
- Peek
- 源码 Source
- 反汇编 Disassemble
-
分析 Profile
- 网页
- 可视化终端
-
实际分析排查过程
-
排查CPU问题
- 命令行分析
- top命令
- list命令
- 熟悉web页面分析
- 调用关系图、火焰图
- 排查堆内存问题
- 排查协程问题
- 排查锁问题
- 排查堵塞问题
-
pprof的采样过程和原理
- CPU采样
- 堆内存采样
- 协程和系统线程采样
- 阻塞操作和锁竞争采样
性能调优案例
-
基本概念
- 服务:能单独部署,承载一定功能的程序
- 依赖:Service A的功能实现依赖Service B的响应结果,称为Service A依赖Service B
- 调用链路:能支持一个接口请求的相关服务集合以及相互之间的依赖关系
- 基础库:公共的工具包、中间件
-
业务优化
-
流程
- 建立服务性能评估手段
- 分析性能数据,定位性能瓶颈
- 重点优化项改造
- 优化效果验证
-
建立压评测估链路
- 服务性能评估
- 构造请求流量
- 压测范围
- 性能数据采集
-
分析性能火焰图,定位性能瓶颈
-
重点优化项分析
- 规范组件库使用
- 高并发场景优化
- 增加代码检查规范避免增量劣化出现
- 优化正确性验证
-
上线验证评估
- 逐步放量
-
进一步优化,服务整体链路分析
- 规范上有服务调用接口,明确场景需求
- 分析业务流程,通过业务流程优化提升服务性能
-