本文是记录2022年5月字节跳动青训营的课程笔记!!!
要点:
如何编写更简洁清晰的代码
常用Go语言程序优化手段
熟悉Go程序性能分析工具
了解工程中性能优化的原则和流程
1.高质量编程
什么是高质量?编写的代码能够达到正确可靠、简洁清晰的目标可称之为高质量代码
正确性:是否考虑各种边界条件,错误的调用是否能够处理
可靠性:异常情况或者错误的处理是否明确,依赖的服务出现异常是否能够处理
简洁:逻辑是否简单,后续调整功能或者新增功能是否能够快速支持
清晰:其他人在阅读理解代码的时候是否能清楚明白,重构或者修改功能不会担心出现无法预料的问题
简介
编程原则:
简单性:消除“多余的复杂性”,以简单清晰的逻辑编写代码;
不理解的代码无法修复改进;
可读性:代码是写给人看的,而不是机器;
编写可维护代码的第一步是确保代码可读;
生产力:团队整体工作效率非常重要;
编码规范:
推荐使用gofmt自动格式化代码
注释:注释应该解释代码作用;解释代码如何做的;解释代码实现的原因:解释代码什么情况会出错;
Good code has lots of comments, bad code requires lots of comments
—— Dave Thomas and Andrew Hunt
注释应该解释代码的作用:
注释公共符号:
注释实现过程:
适合解释代码的外部因素 提供额外的上下文:
解释代码的限制条件:
公共符号的注释:Google Style指南中有两条规则(1.任何既不明显也不简短的功能必须予以注释。2;无论长度或复杂度如何,对库中的任何函数都必须进行注释)
注:不需要注释实现接口的方法,公共符号要始终注释
编码命名规范:
variable:
简洁胜于冗长
缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
变量距离其实被使用的地方越远,则需要携带越多的上下文信息
function:
函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的
函数名尽量简短
package:
只由小写字母组成,不包含大写字母和下划线等字符
简短并包含一定的上下文信息
不要与标准库同名
以下规则尽量满足:
不适用常用变量名作为包名
是用单数而不是复数
谨慎的使用缩写
控制流程:
避免嵌套,保持正常流程清晰
保持正常代码路径为最小缩进
Go语言代码不是成功的路径越来越深地嵌套在右边,而是随着函数的执行,正常流程大的代码会沿着屏幕向下移动
一个功能如果可以通过多个功能的线性结合来实现,那么他的结构就会非常简单。反过来用条件分支控制代码、毫无章法地增加状态数等行为会让代码变得难以理解。
如果能让正常流程自上而下、简单清晰地进行处理,代码的可读性就会大幅提高,与此同时可维护性也将提高,添加功能等改良工作也会变得更加容易
故障问题大多出现在复杂的条件语句和循环语句中,在维护这种逻辑时,添加功能会变成高风险的操作,很容易遗漏部分条件导致问题
错误和异常处理:
简单的错误:
简单的错误指的是仅出现一次的错误,且在其他地方不需要捕获该错误
优先使用erroes.New来创建匿名变量来直接表示该错误
如果有格式化的需求,使用fmt.Errorf
错误的Wrap和Unwrap
错误的Wrap实际上时提供了一个error嵌套在另一个error的能力,从而生成一个error的跟踪链
在fmt.Errorf中使用:%w关键字来将一个错误关联至错误链中
错误判定:
判定一个错误是否为特定错误,使用errors.Is
不同于使用== ,该方法可以判定错误链上的所有错误是否含有特定的错误
在错误链上获取特定种类的错误,使用errors.As
panic:
不建议在业务中使用panic(因为panic发生后,会向上传播至调用栈顶,如果当前goroutine中所有defer函数都不包含recover就会造成整个程序崩溃)
当程序启动阶段发生不可逆转的错误时,可以在init或main函数中使用panic
recover:
recover只能被defer的函数中使用
嵌套无法生效
只有在当前goroutine生效
defer的语句是后进先出
如果需要更多的上下文信息,可以recover后在log中记录当前的调用栈
性能优化建议
性能优化的前提是满足正确可靠、简洁清晰等质量因素
性能优化综合评估,有时候时间效率和空间效率可能对立
Benchmark
Go语言提供了支持基准性能测试的benchmark工具
以计算斐波拉契数列的函数为例,分为两个文件,fib.go编写函数代码,fib_test.go编写benchmark的逻辑,通过命令benchmark可以得到预测结果
-benchmaem表示统计内存信息
slice:
slice预分配内存(尽可能在使用make()初始化切片时提供容量信息)
切片本质是一个数组片段的描述:包括数组指针,片段的长度,片段的容量(不改变内存分配下的最大长度)
切片操作并不复制切片指向的元素
创建一个新的切片会复用原来切片的底层数组(当append之后的长度小于等于cap,将会直接里永远底层数组的剩余空间,当append后长度大于cap则会分配一块更大的区域来容纳新的底层数组)
新的问题:大内存未释放
原切片由大量的元素构成,但是我们在原切片的基础上切片,虽然只使用了很小的一段,但底层数组在内存中仍然占据了大量空间,得不到释放
可以使用copy代替re-slice
map:
同slice,不断地向map中添加元素会触发map的扩容
提前分配好空间可以减少内存拷贝和Rehash的消耗
建议根据实际需求提前预估好需要的空间
字符串处理
常见的字符串拼接方式
实际运行,“+”拼接性能最差,strings.Builder,bytes.Buffer相近,strings.Buffer最快
字符串在Go中是不可变的类型,占用内存空间是固定的
使用+每次都会重新分配内存
strings.Builder,bytes.Buffer底层都是[]byte 数组
内存扩容策略,不需要每次拼接都重新分配内存
空结构体
使用空结构体节省内存
空结构体struct{}实例不占用任何空间
可作为各种场景下的占位符使用
节省资源、空结构体本身具备很强的语义,不需要任何值。仅作为占位符
atomic包
在工作中迟早会遇见多线程编程的场景,比如实现一个多线程公用的的计数器,如何保证计数准确、线程安全有不同的方式
使用atomic包:
锁的实现是通过操作系统来实现的,属于系统调用
atomic操作通过硬件实现,效率比锁搞
sync.Mutex应该保护一段逻辑,不仅仅用于保护一个变量
对于非数值操作,可以使用atomic.Value能承载一个interface{}
性能优化总结:
避免常见的性能陷阱可以保证大部分程序的性能
普通应用代码,不要一味的追求程序的性能
越高级的优化手段越容易出现问题
在满足正确可靠、简洁清晰的质量要求的前提下提高程序性能
2.性能分析工具 pprof
性能调优的前提是对应用程序性能表现有实际的数据指标,对于Go程序,有一个很方便的工具就是pprof
这里有一个开源项目,已经制造了一些问题代码,需要进行排查
wolfogre/go-pprof-practice: go pprof practice. (github.com)
实际分析排查过程
-
排查 CPU 问题
-
命令行分析
-
go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"
- top 命令
-
list 命令
-
熟悉 web 页面分析
-
调用关系图,火焰图
-
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/cpu"
-
-
排查堆内存问题
- go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
-
排查协程问题
- go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"
-
排查锁问题
- go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"
-
排查阻塞问题
- go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"
pprof 的采样过程和原理
- CPU 采样
- 堆内存采样
- 协程和系统线程采样
- 阻塞操作和锁竞争采样
CPU采样
CPU采样会记录所有调用栈和他们所占用的时间
在采样时,进程会每秒暂停一百次,每次记录当前的调度信息。汇总后,根据调用栈在采样中出现的次数来推断函数的运行时间
堆内存采样
采样程序通过内存分配器在堆上分配和释放的内存,记录分配/释放的内存大小和数量
采样率:每分配512KB记录一次
采样时间:从程序运行开始到采样时
采样指标:alloc_space,alloc_objects,inuse_spac,inuse_objects
计算方式:inuse = alloc - free
协程和系统线程采样
协程:记录所有用户发起且在运行中的协程(即入口非runtime开头的)runtime.main的调用栈信息
线程:记录程序创建所有系统线程的信息
阻塞操作和锁竞争采样
业务优化
流程:
- 建立服务性能评估手段
- 分析性能数据,定位性能瓶颈
- 重点优化项改造
- 优化效果验证
建立服务性能评估手段
之所以不用benchmark是因为时间服务逻辑比较复杂,希望从更高的层面分析服务的性能问题,同时机器在不同负载下的性能也会不同
评估手段建立后,产出一个服务的性能指标分析报告
有了服务优化前的性能报告和一些性能采样数据,我们可以进行性能瓶颈分析
业务服务常见的性能问题可能是使用的基础组件不规范
类似日志使用不归发,一部分是调试日志发布到线上,一部分是线上服务在不同的调用链路上数据有差别,测试场景日志还好,但是到了真实线上全景场景,会导致日志数量增加影响性能
另外常见的性能问题就是高并发场景的优化不足
重点优化项改造:
正确性实际基础!
性能优化的前提是保证正确性,使用在变动较大的性能优化上线之前,需要进行正确性验证,借助自动化手段保证优化后程序的正确性
优化效果验证:
用同样的数据对优化后的服务进行压测,同时压测并不能保证和线上表现完全一致,有时还要通过线上的表现在进行分析改进,是个长期的过程
基础库优化
适应范围更广,覆盖更多服务
AB 实验 SDK 的优化:
- 分析基础库核心逻辑和性能瓶颈
- 完善改造方案,按需获取,序列化协议优化
- 内部压测验证
- 推广业务服务落地验证
Go语言优化
适应范围最广,Go 服务都有收益
优化方式
- 优化内存分配策略
- 优化代码编译流程,生成更高效的程序
- 内部压测验证
- 推广业务服务落地验证
总结
性能评估要依靠数据,用实际的结果做决策
对于pprof工具,可以通过分析实际的程序,熟悉相关功能,理解好基本原理,后续能够更好的解决性能问题
在真正的服务性能调优流程中,链路会很长,重点是要保证正确性,不影响功能,同时定位好问题
参考资料:
-
注释应该解释代码作用
- 适合注释公共符号,github.com/golang/go/b…
-
注释应该解释代码如何做的
- 适合注释方法,github.com/golang/go/b…
-
注释应该解释代码实现的原因
- 解释代码的外部因素,github.com/golang/go/b…
- 注释应该解释代码什么情况会出错
-
公共符号始终要注释
- 包中声明的每个公共的符号:变量、常量、函数以及结构都需要添加注释
- github.com/golang/go/b…
- github.com/golang/go/b…
- 熟悉 Go 语言基础后的必读内容,go.dev/doc/effecti…
- Dave Cheney 关于 Go 语言编程实践的演讲记录,dave.cheney.net/practical-g…
- 《编程的原则:改善代码质量的101个方法》,总结了很多编程原则,按照是什么 -> 为什么 -> 怎么做进行了说明,mp.weixin.qq.com/s/vXSZOl2Gt…
- 如何编写整洁的 Go 代码,github.com/Pungyeon/cl…
- Go 官方博客,有关于 Go 的最新进展,go.dev/blog/
- Dave Cheney 关于 Go 语言编程高性能编程的介绍,dave.cheney.net/high-perfor…
- Go 语言高性能编程,博主总结了 Go 编程的一些性能建议, geektutu.com/post/high-p…
- Google 其他编程语言编码规范,可以对照参考,zh-google-styleguide.readthedocs.io/en/latest/
- Go 代码 Review 建议github.com/golang/go/w…
- Uber 的 Go 编码规范,github.com/uber-go/gui…