这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天
高质量编程与性能调优
高质量编程
我们编写的代码主要有三个主要的评判标准:
- 边界条件考虑是否完备
- 异常处理,稳定性保证
- 易读易维护
编程原则
简单性
- 消除多余的复杂性,以简单清晰的逻辑编写代码
- 不理解的代码无法进行修复和改进
可读性
- 代码是给人看的,不是机器
- 编写可维护的代码第一步是要确保代码可读
生产力
- 团队整体工作效率十分重要
编码规范
gofmt自动格式化代码,或者goimport(相当于gofmt加上包依赖管理)
注释
注释是用来:
-
解释代码的作用
注释公共符号,如对变量的注释,对函数的注释,或者是库中对外公开的函数的注释。
-
解释代码如何运行
注释功能的实现过程,对复杂的逻辑需要进行说明
-
解释代码为什么要这么做
解释代码的外部影响因素,并提供额外的上下文信息,帮助阅读者理解代码为什么要这么做
-
解释代码何时会有错误
解释代码的限制条件
对于公共符号:
- 包中声明的每一个公共符号:变量常量函数以及结构都需要添加注释
- 任何既不明显也不简短的公共功能必须注释
- 无论长度或复杂度如何,库中对外的提供的任何函数也需要注释(文档)
- 不需要注释实现接口的方法
总的来说,代码是最好的注释。注释要提供代码未表达出的上下文信息,来帮助阅读者了解代码所处于的位置功能,与实现细节。可以帮助阅读者对单个函数或者整个项目有更好的了解。
命名规范
变量:
-
简介比冗长好
例如在for循环中定义一个下标。使用
i比index要好 -
缩写词需要大写,如果缩写词在变量开头,且不需要导出时,可以使用小写
ServerHTTP,xmlHTTPRequest -
变量距离其被使用的地方越远。则需要携带越多的上下文信息
全局变量在名字中需要更多的上下文信息,使得在不同的地方可以辨认出其含义
函数:
- 函数名不需要携带包的上下文信息,因为包名和函数总是成对出现
- 函数名尽可能简短
包:
- 只由小写字母组成,不包含大写字母和下划线
- 简短并且包含一定的上下文信息
- 不用于标准库重名
包要以下要求尽量满足:
- 不使用常用变量名作为包名
- 使用单数而不是复数
- 谨慎使用缩写
核心目标时降低阅读理解代码的成本,重点考虑上下文信息,设计简洁清晰的名称。我们的所有的名称,都是有意义的,尽管不同的命名所带有的信息不同。我们要根据,我们需要提供什么数据,以及对方需要什么数据,两方面出发。才能确定一个合理且明晰的名称。
流程控制
-
避免流程的嵌套。保持正常的流程清晰
-
尽量保持正常代码路径为最小缩进
优先处理错误或者特殊情况,尽早返回或继续循环来减少嵌套。
func OneFunc() error { err := doSomething() if err == nil { err := doAnother() if err := nil { return nil } return err } return err }可以转化为:
func OneFunc() error { if err := doSomething(); err != nil { return err } if err := doAnother(); err != nil { return err } return nil }
流程控制我们有以下的总结:
- 线性原理,处理逻辑尽量走直线,避免复杂的分支嵌套
-
正常流程代码沿着屏幕向下移动
和第一点一块看的话,其实就是在流程图中,我们的从顶向下我们的正确路径是在树干上的。而错误处理是在支路上。
- 提升代码的可维护性和可读性
- 故障问题大多出现在复杂的条件语句和循环语句中
错误与异常
简单错误:
- 简单错误是仅仅出现一次的错误,且在其他地方不需要捕获该错误
- 优先使用errors.New来创建匿名变量来直接表示简单错误
- 如果有格式化需求,使用fmt.Errorf
错误的Wrap与Unwrap:
- Wrap实际上提供了一个error嵌套另一个error的能力,生成一个error的跟踪链
- 在fmt.Errorf中使用%w来将一个错误关联至错误链
错误判定:
- 判定一个错误是否为特定的错误,使用errors.Is
- 不同于使用==,该方法可以判断错误连上的所有错误中,是否有特定的错误
- 在链上获取特定种类的错误,使用errors.As
panic:
- 不建议在业务代码中使用panic
- 调用函数不包含recover会导致程序崩溃
- 如果问题可以被屏蔽或者解决,建议使用error代替panic
- 当程序启动阶段发生不可逆转的错误的时,可以在init函数或者main函数中使用panic
recover:
- recover只能在被defer的函数中使用
- 嵌套无法生效
- 只在当前的goroutine生效
- defer的语句是先进后出
- 如果需要更多的上下文信息,可以revcover后在log中记录当前的调用栈
error尽可能提供简明的上下文信息链,方便定位问题
panic用于真正异常的情况
recover生效范围,在当前的goroutine的defer生效
性能优化
- 性能优化的前提是满足正确可靠,简介清晰等质量因素
- 性能优化是综合评估,时间效率与空间效率可能会对立
Benchmark:
- 性能表现需要实际数据衡量
- benchmark是go语言的基准性能测试工具
能够预分配空间可以大大提高效率。
对于slice,多个slice可能会复用一块地址。可以使用copy来减小内存消耗。
字符串拼接 可以使用string.Builder来提升效率
空结构体可以节省内存。(map实现set)
多线程通信建议使用atomic包
实践:
- 依靠数据而不是猜测
- 定位最大瓶颈而不是细枝末节
- 不过早优化
- 不过度优化
主要使用pprof,可视化和分析性能分析数据工具。