高质量编程
简介
- 编写的代码能够达到正确可靠、简洁清晰的目标可以称之为高质量代码
- 需要考虑各种边界条件是否完备
- 异常情况处理是否妥善,能否保证稳定性
- 是否易读易维护
编程原则:
- 简单性
- 消除“多余的复杂性”,以简单清晰的逻辑编写代码
- 不理解的代码无法修复改进
- 可读性
- 编写可维护代码的第一步是确保代码可读
- 生产力
- 团队整体工作效率非常重要
编码规范
代码格式
- 使用 gofmt 自动格式化代码
- 使用 goimport 实现自动增删依赖的包引用、将依赖包按字母序排序并分类
注释
- 注释应该解释代码的作用
- 注释应该解释代码如何的(实现过程)
- 注释应该解释代码实现的原因
- 解释代码的外部因素
- 提供额外上下文
- 注释应该解释代码什么情况会出错
- 公共符号始终要注释
- 变量、常量、函数以及结构体
- 任何既不明显也不简短的公共功能
- 无论长度或复杂长度,库中的任何函数必须进行注释
- 不需要注释实现接口的方法
命名规范
- 变量命名规范
- 略缩词全部大写,但当其位于变量开头且不需要导出时,使用全小写
- 例如使用 ServeHTTP 和 XMLHTTPRequest 而不是 ServeHttp 和 xmlHTTPRequest
- 变量距离其被使用的地方越远,则需要携带更多的上下文信息
- 如全局变量在其名字中应该包含更多上下文信息
- 对于一些只是局部变量的,不包含特殊含义的,没有必要使用全程,只需要特定的单词或者字母缩写替代即可,如 for 循环中的 i
- 对于方法参数中带有特殊含义的,不要将其替换为简写,因为这会降低变量名的信息量
- 略缩词全部大写,但当其位于变量开头且不需要导出时,使用全小写
- 函数名命名规范
- 函数名不携带报名的上下文信息,因为包名和函数名是成对出现的
- 当名为 foo 的包的某个函数返回类型为 Foo 时,可以省略类型信息避免导致歧义
- 当名为 foo 的包的某个函数返回类型为 T 而不是 Foo 的时候,可以在函数名中加入类型信息
- 包名命名规范
- 只由小写字母组成,不能包含大写字母和下划线
- 在包含一定的上下文信息的前提下,做到包名尽量简短
- 不要和标准库同名
- 不要使用常用变量名作为包名
- 使用单数而不是复数
- 谨慎使用缩写,需要保证不破坏上下文的情况下使用缩写
控制流程
- 避免无意义的嵌套如
-
if foo{ return true }else { return false } ↓ if foo{ return true } return false
- 尽量保持正常代码路径为最小缩进
- 优先处理错误情况/特殊情况,使得可以尽早返回或继续循环来减少嵌套
- 使用线性原理——处理逻辑尽量走直线
- 保证正常流程代码沿着屏幕向下移动
错误和异常处理
- 简单错误:仅出现一次的错误,且在其他地方不需要捕获该错误
- 优先使用 errors.New 来创建匿名变量来直接表示简单错误
- 如果需要格式化使用 fmt.Errorf
- 复杂错误
- 使用 Wrap 和 Unwrap
- Wrap 的原理实际上是提供了一个 error 嵌套另一个 error 的能力,从而生成跟踪链
- 在 fmt.Errorf 可以使用 %w 关键字来讲错误关联至错误链中
- 错误判定
- 判断是否是特定错误可以使用 errors.is
- 与 == 的区别时可以判断错误链上的所有错误
- 使用 errors.As 方法可以从错误链上获得特定种类的错误
- panic 时在程序启动阶段发生不可逆转的错误的时候,才考虑在 init 或 main函数中使用 panic,但是如果调用函数不包含 recover 会造成程序崩溃,因此如果可以被覆盖建议使用 error
- recover
- recover 只能在被 defer 的函数中使用
- 嵌套无法生效
- 只在当前的 goroutine 生效
- defer 的语句是后进先出的
性能优化建议
- 可以使用 benchmark 进行分析,得到执行的次数、每次执行花费的时间、每次执行申请多大的内存、每次执行申请几次内存
- slice 在使用的时候尽可能提供容量信息,预分配内存会提高速度
- 切片本质上是一个数组片段的描述,他并不复制切片指向的元素,而是会创建一个新的切片复用原来切片的底层数组,这样就会导致增加时间,同时因为是在已有的切片基础上创建切片,那么原本的底层数组在内存中有引用得不到释放,就会占用内存,此时可以用 copy 替代 re-slice 方法
- map 也推荐预分配内存
- 由于向 map 中不断添加元素的操作会触发 map 的扩容,因此提前分配好内存空间可以减少内存拷贝和 rehash 的消耗
- 在进行字符串拼接的时候,建议使用 strings.Builder 而不要直接使用 +
- 原因是在于字符串在 Go 语言中是不可变类型,占用内存的大小是固定的,使用 + 进行操作会导致每次都需要重新分配内存,而 strings.Builder 和 byte.Buffer 的底层都是 byte 数组,因此不需要每次拼接都重新分配内存
- 使用空结构体来节省内存
- 因为空结构体的实例不占据任何的内存空间,可以作为任何场景下的占位符使用
- 可以用在 map 实现 set
- atomic 包
- 由于锁的实现是基于系统的调用,而 atomic 操作是属于硬件的实现,因此atomic 的效率要远远高于锁的效率
- 相较于使用sync.Mutex, 是应该用来保护一段逻辑的,而不仅仅用于保护一个变量,仅仅保护一个变量的话使用 atomic 即可
- 对于非数值操作,可以使用 atomic.Value
性能调优实战
- 原则:
- 要依靠数据不是猜测
- 要定位最大瓶颈而不是细枝末节
- 不要过早优化
- 不要过度优化
pprof
- flat == cum 代表没有调用其他函数,flat == 0 代表函数只有其他函数的调用
性能调优案例
- 业务服务优化
- 服务:能单独部署,承载一定功能的程序
- 依赖:对某个响应结果有需求
- 调用链路:能支持一个接口请求的相关服务集合及其相互之间的依赖关系
- 基础库:公共的工具包、中间件