这是我参与「第五届青训营 」笔记创作活动的第3天
只不过是字节给我的任务罢了
高质量编程
高质量——代码清晰可靠,简洁清晰
- 各种边界条件是否完备
- 异常情况处理
- 易读易维护
编程原则
简单性:消除多于的复杂性,以简单清晰的逻辑编写代码
可读性:代码要易读,以便维护
生产力:团队工作效率很重要
如何编写更简洁清晰的代码
-
公共符号必须注释
- 公共的函数、变量、常量以及结构必须注释
- 库中提供的任何函数必须注释
- 不需要注释实现接口的方法
-
代码格式
- 使用gofmt自动格式化go语言代码
- 使用goimports自动增删依赖包
-
注释
- 解释代码作用
- 解释代码如何做的(对功能不明显的复杂代码进行注释)
- 代码实现的原因
- 解释代码什么情况出错
- 代码是最好的注释
- 注释应该提供代码未表达的上下文信息
-
命名规范
-
变量名
- 简洁胜于冗长
- 缩略词全大写,如果缩略词位于开头且不需要导出时,使用全小写
- 变量距离被使用的语句越远,则越需要携带更多的上下文信息
- 命名变量时,尽量见名知意,而非单纯的使用单个字母
-
函数名
- 函数名不携带包名的上下文信息
- 函数名尽量简短
- 当包名和包内函数名相似时,可以不在函数名中添加类型信息
- 如果包名和包内函数名不相似时,需要在函数名中添加类型信息
-
包名
-
只能由小写字母组成,不要包含大写字母和下划线
-
不要与标准库同名
-
尽量满足
- 不使用常用变量名作为包名
- 单词使用单数而非复数
- 谨慎使用缩写
-
-
-
控制流程
- 避免嵌套,尽量走线性流程,保证正常流程清晰
- 优先处理错误情况/特殊情况,尽早返回或继续循环来减少嵌套
- 尽量保证正常代码路径为最小缩进
-
错误和异常处理
-
简单错误
- 简单的错误指仅出现一次的错误
- 优先使用
error.New()
创建匿名变量来直接表示简单错误 - 如果有格式化要求,使用
fmt.Errorf()
-
错误的Wrap和Unwrap
- 错误的Wrap实际上提供了一个error嵌套另一个error的能力,从而生成error跟踪链
- 在
fmt.Errorf()
中使用%w
关键字将一个错误关联至错误链中
-
错误判定
- 使用
Error.Is(err, ErrorName)
判断是否为特定错误 - 使用
error.As(err, &pathError)
在错误链上获取特定的错误
- 使用
-
panic
- 不建议在业务代码中使用panic
- 调用函数如果不包含recover会造成程序崩溃
- 如果问题可以被屏蔽或解决,建议使用error代替panic
- 当启动阶段发生不可逆转的错误时,可以在init或main函数中使用panic
-
recover
-
recover只能在被defer的函数中使用
-
嵌套无法生效
-
只在当前goroutine生效
-
defer语句是后进先出
-
如果需要更多的上下文信息,可以在recover后在log中记录当前的调用栈
defer func() { if e := recover(); e != nil { f = nil err = fmt.Errorf("gitfs panic: %v\n%s", e, debug.Stack()) } }()
-
-
Go性能优化
常用Go语言程序优化手段
Benchmark
go test -bench=. -benchmem # 运行基准测试
slice预分配内存
尽可能在初始化时,提供初始容量信息,减少底层内存分配次数
切片的本质是一个数组片段的描述
type slice struct {
array unsafe.Pointer // 数组指针
len int // 片段长度
cap int // 片段容量(不改变内存分配的情况下的最大长度)
}
切片操作并不复制切片指向的元素
创建一个切片会复用原来切片的底层数组
因为这一特性,可能会导致一种情况
-
大内存未释放
原切片较大,代码会在原切片基础上新建小切片,原底层数组在内存中有引用,得不到释放,可以使用copy()函数代替在原有基础上新建小切片
map预分配内存
尽可能在初始化时,提供初始容量信息
不断地向map添加元素可能会触发map扩容,增加内存分配次数,因此尽可能根据需求提前分配好空间,减少内存拷贝和Rehash的消耗
字符串处理
使用+
拼接的性能最差,string.Builder()
和bytes.Buffer()
性能相近,前者更快
使用+
每次都会重新分配内存,而后两者的底层都是[]byte数组,使用内存扩容策略,不需要每次重新分配内存
空结构体
使用空结构体节省内存,空结构体不占据任何内存空间,可以作为各个场景下的占位符使用
m := make(map[int]struct{})
m[0] = struct{}{}
m := make(map[int]bool)
m[0] = false
实现set可以考虑使用空结构体代替map
set只需要key,不需要value,如果使用map,即时value类型为bool,也会占用1byte空间,而空结构体不占据任何内存空间
atomic包
锁的实现是通过操作系统实现的,属于系统调用
atomic操作是通过硬件实现,效率比锁高
sync.Mutex应该保护一段逻辑,而非一个变量
对于非数值操作,可以使用atomic.Value,能承载一个interface{}
使用atomic包代替使用性能差的sync.Mutex
type atomicCounter struct {
i int32
}
func AtomicAddOne(c *atomicCounter) {
atomic.AddInt32(&c.i, 1)
}
性能优化的原则和流程
-
性能调优原则
- 要依靠数据,而非猜测
- 要定位最主要的瓶颈,而非细枝末节
- 不要过早优化,等到业务逻辑和功能确定后,性能出现问题后再优化
- 不要过度优化
Go程序性能分析工具
性能分析工具pprof
-
用于可视化和分析性能分析数据的工具
-
可以查看CPU、Memory消耗
-
查看CPU消耗
topN
go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10" (pprof)top
- flat 当前函数本身执行耗时
- flat% flat占CPU总时间的比例
- sum% 上面每一行flat%总和
- cum 指当前函数本身加上其调用函数的总耗时
- cum% cum占CPU总时间的比例
- flat==cum函数中没有调用其他函数;flat==0函数中只有其他函数的调用
-
根据指定到正则表达式查找代码行
list re
-
调用关系可视化
web
(pprof)web
- 这个命令需要软件支持graphviz.gitlab.io/download/
- 如果安装了上面的软件,在终端(Windows)里执行
web
仍然报错Failed to execute dot. Is Graphviz installed? Error: exec: "dot": executable file not found in %PATH%
,以管理员打开cmd,进入软件安装路径(进入到安装路径的bin文件夹),执行dot -c
,再随便打开一个cmd执行dot -version
,验证是否成功,如果使用Goland的Terminal仍然报错Failed to execute dot. Is Graphviz installed? Error: exec: "dot": executable file not found in %PATH%
,重启Goland即可
-
查看堆内存Heap
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
在弹出的页面里可以更改视图,或者使用选项卡里的更多功能,查看不同信息
-
SAMPLE
- alloc_objects程序累计申请对象数
- inuse_objects程序当前持有对象数
- alloc_space程序累计申请内存大小
- inuse_space程序当前占用内存大小
-
-
Goroutine
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"
VIEW——FlameGraph可以查看火焰图
-
Mutex
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"
-
block
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"
pprof采样过程和原理
-
CPU采样
-
采样对象函数调用和其占用的时间
-
采样率:100次/s,固定值
-
采样时间:从手动启动到手动结束
-
开始采样——设定信号处理函数——开启定时器
- 进程启动定时器
- 操作系统:每10ms向进程发送一次SIGPROF信号
- 进程:进程收到OS发来的SIGPROF后记录调用堆栈,然后启动写缓冲
- 写缓冲:启动后,每100ms读取已经记录的调用栈并写入输出流
- 进程结束定时器,结束写缓冲输出
-
停止采样——取消信号处理函数——关闭定时器
-
-
Heap堆内存采样
- 采样程序通过内存分配器在堆上分配和释放内存,记录分配和释放的大小和数量
- 采样率:每分配512KB记录一次,可以在运行开头修改(1为每次分配都记录)
- 采样时间:程序运行开始到采样时
- 采样指标:alloc_objects、inuse_objects、alloc_space、inuse_space
- 计算方式:inuse = alloc - free
-
Goroutine协程&ThreadCreate线程创建
- 记录所有用户发起且在运行中的Goroutine(入口非runtime开头的)runtime.main的调用栈信息
- Stop The World——遍历allg切片——输出创建g的堆栈——Start The World
-
ThreadCreate
- 记录程序创建的所有系统线程的信息
- Stop The World——遍历allm链表——输出创建m的堆栈——Start The World
-
Block阻塞和Mutex锁
-
阻塞操作
- 采样阻塞操作的次数和耗时
- 采样率:阻塞耗时超过阈值的才会被记录,时间未到则丢弃,1为每次都记录
-
锁竞争
-
采样争抢锁的次数和耗时
- 采样率:只记录固定比例的锁操作,比例未到则丢弃,1为每次加锁均记录
-
-
性能调优案例
业务服务优化
-
基本概念
- 服务:能单独部署,承载一定功能的程序
- 依赖:ServiceA依赖ServiceB的响应结果,叫做ServiceA依赖ServiceB
- 调用链路:能支持一个接口请求的相关服务集合及其相互之间的依赖关系
- 基础库:公共的工具包、中间件
-
优化流程
-
建立服务性能评估手段
-
服务性能评估方式
- 单独的benchmark无法满足复杂逻辑分析
- 不同负载情况下性能表现差异
-
请求流量构造
- 不同请求参数覆盖逻辑不同
- 线上真实流量情况
-
压测范围
- 单机器压测
- 集群压测
-
性能数据采集
- 单机性能数据
- 集群性能数据
-
-
分析性能数据,定位性能瓶颈
- 使用火焰图查看高并发场景的优化问题
-
重点优化改造
-
正确性是基础
-
响应数据diff
- 线上请求数据录制回放
- 新旧逻辑接口数据diff
-
-
优化效果验证
-
重复压测验证
-
上线评估优化效果
- 关注服务监控
- 逐步放量
- 收集性能数据
-
-
进一步优化,服务整体链路分析
- 规范上游服务调用接口,明确场景需求
- 分析链路,通过业务流程优化提升服务性能
-
基础库优化
-
AB实验SDK的优化
-
分析基础库核心逻辑和性能瓶颈
- 设计完善改造方案
- 数据按需获取
- 数据序列化协议优化
-
内部压测验证
-
推广业务服务落地验证
-
Go语言优化
-
编译器&运行时优化
-
优化内存分配策略
-
优化编译流程,生成更高效的程序
-
内部压测验证
-
推广业务服务落地验证
-
优点
- 接入简单,只需要调整编译配置
- 通用性强
-