这是我参与「第五届青训营 」伴学笔记创作活动的第4天。
高质量编程
高质量代码,编写的代码能够达到正确可靠、简洁清晰的目标。
正确,各种边界条件是否考虑完备; 可靠,异常情况能否处理,能否保证稳定性; 简洁清晰,逻辑简单,易读易维护;
编程原则
虽然实际应用场景,各种语言特性语法不同,但是高质量编程遵循的原则是相通的。
- 简单性,消除“多余的复杂性”,以简单清晰的逻辑编写代码;不理解的代码难以修复改进
- 可读性,代码是写给人看的,而不是机器;编写可维护代码的第一步是确保代码可读
- 生产力,团队整体工作效率非常重要
实践规范
代码格式
官方提供的格式化工具
go fmt:自动格式化代码,常见IDE都支持配置
go imports:等于go fmt加上依赖包管理,可以自动增删依赖的包引用,将依赖包按字母排序并分类
注释
- 注释应该解释代码作用。 适合注释公共符号,变量、常量、函数以及结构体
- 注释应该解释代码如何做的。 适合注释实现过程
- 注释应该解释代码实现的原因。 适合解释代码的外部因素,提供额外上下文
- 注释应该解释代码什么情况下会出错。 适合解释代码的限制条件
无论长度或复杂度如何,任何函数都应该进行注释。实现接口的方法不需要注释
命名规范
- 核心目标,降低阅读理解代码的成本;重点考虑上下文信息,设计简洁清晰的名称
-
变量
简洁;
缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写;
变量距离使用位置越远,则应携带越多的上下文信息,全局变量;
-
函数
不携带包名的上下文信息;
尽量简短;
当函数提供外部调用时,签名信息应该详细
-
包
只由小写字母组成,不包含大写字母和下划线;
简短并包含一定的上下文信息;
不要与标准库同名;
不使用常用变量名作为包名;
使用单数而不是复数;
谨慎使用缩写
-
控制流程
避免if-else嵌套,保持正常流程清晰
尽量保持正常代码路径为最小缩进,优先处理错误情况/特殊情况
线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支
-
错误和异常处理
-
简单错误
仅出现一次,且在其他地方不需要捕获该错误;
使用errors.New()来创建匿名变量来直接表示简单错误;
如果有格式化要求,使用fmt.Errof()
-
复杂错误
错误的Wrap,提供一个error嵌套另一个error的能力,从而生成一个error的跟踪链,好处是每层调用方可以补充自己对应的上下文信息,方便跟踪排查问题;
fmt.Errof中使用"%w"将一个错误关联至错误链中
if err!=nil{ return fmt.Errof("reading error:%w",err) }判断错误链上的所有错误是否含有特定错误,使用errors.Is()
获取错误链上的特定种类的错误,使用errors.As()
-
-
panic
不建议在业务代码中使用panic,若问题可以被屏蔽或解决,建议使用error代替panic;
当程序启动阶段发生不可逆转的错误时,可以在init或main函数中使用panic;
-
recover
出现panic后可使用reover记录上下文信息,可以在recover后在log中记录当前的调用栈debug.Stack(),方便分析定位问题
只能在defer函数中使用;
defer函数:嵌套无法生效;只在当前goroutine生效;多个defer函数后进先出
func (t treeFS)Open(name string)(f fs.File,err error){ defer func(){ if e:=recover();e!=nil{ f=nil err=fmt.Errof("gitfs panic:%v\n%s",e,debug.Stack()) } } } -
小结
error 尽可能提供简明的上下文信息链,方便定位问题panic 用于真正异常的情况
recover 在当前goroutine的defer函数中生效
性能调优
性能优化的前提是满足正确可靠、简洁清晰等质量因素
性能优化是综合评估,需要综合考虑时间效率和空间效率
评估性能
性能表现需要实际数据衡量
Go语言提供了支持基准性能测试的benchmark工具
基准测试的代码文件必须以_test.go结尾
基准测试的函数必须以Benchmark开头,必须是可导出的
基准测试函数必须接受一个指向Benchmark类型的指针作为唯一参数
基准测试函数不能有返回值
func BenchmarkSprintf(b *testing.B){
num:=10
b.ResetTimer()
for i:=0;i<b.N;i++{
fmt.Sprintf("%d",num)
}
}
b.ResetTimer是重置计时器,这样可以避免for循环之前的初始化代码的干扰。最后的for循环很重要,被测试的代码要放到循环里。b.N是基准测试框架提供的,表示循环的次数,因为需要反复调用测试的代码,才可以评估性能。
go test -bench=. -benchmem //-bench=,接受一个表达式作为参数,.表示运行所有的基准测试,因为默认情况下 go test 会运行单元测试;-benchmem可以提供每次操作分配内存的次数,以及每次操作分配的字节数,找到根本原因。
优化建议
-
slice预分配内存
尽可能在使用make()初始化切片时提供容量
大切片基础上创建小切片,使用copy(des,src),避免大内存未释放
-
map预分配内存
根据需求提前预估好需要的空间,因为map扩容会有内存拷贝和Rehash的消耗
-
字符串处理
使用strings.Buider;使用+拼接性能最差,strings.Builder和bytes.Buffer相近,bytes.Buffer更快;
原因:字符串在 Go 语言中是不可变类型,占用内存大小是固定的,使用 + 每次都会重新分配内存。strings.Builder,bytes.Buffer 底层都是[]byte 数组,内部维护内存扩容策略,不需要每次拼接重新分配内存。bytes.Buffer 转化为字符串时重新申请了一块空间,strings.Builder 直接将底层的[]byte 转换成了字符串类型返回。
-
空结构体
使用空结构体节省内存,空结构体struct{}不占内存空间,可作为各种场景下的占位符使用
map+空结构体可以实现set
-
atomic包
多线程情境下,可用于保证计数安全,线程安全,比加锁时间性能好
原因:锁的实现是通过操作系统来实现,属于系统调用。atomic 操作是通过硬件实现,效率比锁高。sync.Mutex 应该用来保护一段逻辑,对于仅保护一个变量可以使用atomic包。对于非数值操作,可以使用 atomic.Value,能承载一个 interface{}
小结
避免常见的性能陷阱就可以保证大部分程序的性能;
普通应用代码,不要一味地追求程序的性能;
越高级的性能优化手段越容易出现问题;
在满足正确可靠、简洁清晰的质量要求的前提下提高程序性能
调优实战
性能调优原则
要依靠数据而不是猜测;
要定位最大瓶颈而不是细枝末节;
不要过早优化;
不要过度优化
性能分析工具pprof
pprof是用于可视化和分析性能的工具,可以知道程序在什么地方耗费了多少CPU,memory
-
运行代码
pprof浏览器查看指标,参考
-
CPU排查
命令行输入 go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10" 。 将一段时间采集到信息输出到文件中,采集10s
-
top命令:查看占用资源最多的函数。
每列指标含义: flat,当前函数本身的执行耗时;flat%,flat占CPU总时间的比例;sum%,上面每一行的flat%总和;cum,当前函数本身加上其调用函数的总耗时;cum%,cum占CPU总时间的比例。 flat=cum,当前函数没有调用其他函数;flat=0,当前函数只有其他函数的调用。
-
list命令
根据指定的正则表达式查找代码行
-
web命令
调用关系可视化 需安装graphviz,把graphviz/bin目录配置到path环境变量,在IDE中使用cmd终端;
-
-
Heap堆内存排查
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
web页面可视化,VIEW标签下的source可以看到每行代码占用内存;
SAMPLE标签下alloc_objects: 程序累计申请的对象数;alloc_space: 程序累计申请的内存大小;inuse_objects: 程序当前持有的对象数;inuse_space: 程序当前占用的内存大小
-
goroutine协程
goroutine泄露也会导致内存泄漏
浏览器打开http://localhost:6060/debug/pprof/ 可看到运行协程数
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine" 再查看火焰图,由上到下表示调用顺序;每一块代表一个函数,越长代表占用 CPU 的时间更长;火焰图是动态的,支持点击块进行分析
-
mutux-锁
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex" 确定有问题的方法,source视图下确定出问题的代码
总结
了解了编码规范,注释,流程结构,命名规范等。学会使用性能分析工具pprof,可以通过命令或网页查看定位资源占用问题,进行性能优化。