【青训营笔记】- 高质量编程及性能优化

58 阅读5分钟

这是我参与「第五届青训营 」笔记创作活动的第3天。

前言

一个好的后端项目不仅仅只是能够很好的完成目标的功能需求,更应该有高质量的代码编程以及较好服务性能,代码是写给人看的,并且项目往往不是单刀赴会,而是团队协作,要写出队员伙伴能够简单快速清晰地了解代码意图是团队合作中关键的一环。性能优化则是在现如今数据爆炸,互联网普及的背景下加大服务器服务吞吐量的有限操作,通过节省不必要的计算资源和空间资源来提高响应请求的数量,因此本节课也是相当重要的一节。

高质量代码

1. 简介

编程原则

实际应用场景千变万化,各种语言的特性和语法各不相同但是高质量编程遵循的原则是相通的

  • 简单性
    • 消除"多余的复杂性",以简单清晰的逻辑编写代码
    • 不理解的代码无法修复改进可读性
  • 可读性
    • 代码是写给人看的,而不是机器
    • 编写可维护代码的第一步是确保代码可读生产力
  • 生产力
    • 团队整体工作效率非常重要

编码规范

如何编写高质量的 Go 代码

  • 代码格式
    • 使用统一的格式化工具gofmt(golang默认配置),或者goimports管理引入的排列
  • 注释
    • 注释应该解释代码作用:公共符号的功能用途
    • 注释应该解释代码如何做的:函数或代码的实现过程
    • 注释应该解释代码实现的原因:适合解释代码的外部因素,提供额外上下文
    • 注释应该解释代码什么情况会出错:适合解释代码的限制条件
  • 命名规范
  1. 对于变量
    • 简洁胜于冗长
    • 缩略词全大写,但变量开头且不需要导出时,使用全小写
    • 变量距离使用地方越远,则需要携带更多上下文信息
  2. 对于函数
    • 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的
    • 函数名尽量简短
    • 当名为 foo 的包某个函数返回类型 Foo 时,可以省略类型信息而不导致歧义
    • 当名为 foo 的包某个函数返回类型 T 时 (T 并不是 Foo),可以在函数名中加入类型信息
  3. 对于包名
    • 只由小写字母组成。不包含大写字母和下划线等字符
    • 简短并包含一定的上下文信息。例如 schema、task 等
    • 不要与标准库同名。例如不要使用 sync 或者 strings

核心目标是降低阅读理解代码的成本重点考虑上下文信息,设计简洁清晰的名称

  • 控制流程
  1. 避免嵌套,保持正确的流程清晰
  2. 尽量保持正常代码路径为最小缩进
// Bad
func OneFunc() error {
   err := doSomething()
   if err == nil {
      err := doAnotherThing()
      if err == nil {
         return nil // normal case
      }
      return err
   }
   return err
}

// Good
func OneFunc() error {
   if err := doSomething(); err != nil {
      return err
   }
   if err := doAnotherThing(); err != nil {
      return err
   }
   return nil // normal case
}
  • 线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支
  • 正常流程代码沿着屏幕向下移动
  • 提升代码可维护性和可读性
  • 故障问题大多出现在复杂的条件语句和循环语句中
  • 错误和异常处理
  1. 简单错误出现一次的直接使用error.New(string),格式化需求使用fmt.ErrorF
  2. 错误的Wrap和Unwrap
    • 错误的 Wrap 实际上是提供了一个 error 嵌套另一个error 的能力,从而生成一个 error 的跟踪
    • 在 fmt.Errorf 中使用: %w 关键字来将一个错误关联至错误链中
list, _, err := c.GetBytes(cache.Subkey(a.actionID, "srcfiles"))
if err != nil {
  return fmt.Errorf("reading srcfiles list: %w", err)
}
  1. 错误判定:
    • 判断一个错误是否为特定错误时,使用errors.Is,不同于使用==。该方法可以判断错误链上的是否包含指定类型错误
    • 在错误链上获取特定种类的错误,使用errors.As
  2. panic:
    • 不建议在业务代码中使用 panic
    • 调用函数不包含 recover 会造成程序崩溃
    • 若问题可以被屏蔽或解决,建议使用error 代替 panic
    • 当程序启动阶段发生不可逆转的错误时可以在 init 或 main 函数中使用 panic
  3. recover:
    • recover 只能在被 defer 的函数中使用
    • 嵌套无法生效
    • 只在当前 goroutine 生效
    • defer 的语句是后进先出
    • 如果需要更多的上下文信息,可以 recover 后在 log 中记录当前的调用栈
  • error 尽可能提供简明的上下文信息链,方便定位问题
  • panic 用于真正异常的情况
  • recover 生效范围,在当前 goroutine 的被 defer 的函数中生效

性能优化

简介

  • 性能优化的前提是满足正确可靠、简洁清晰等质量因素
  • 性能优化是综合评估,有时候时间效率和空间效率可能对立
  • 针对 Go 语言特性,介绍 Go 相关的性能优化建议

Benchmark性能评估

性能表现需要实际数据衡量,Go 语言提供了支持基准性能测试的 benchmark 工具

  • go test -bench=. -benchmen启动评估命令
  • 结果说明
    • 1.png

性能优化切入点

  • slice切片预分配,减少数组复制的时间开销
  • slice新建小切片导致的大内存不释放,用copy代替切片
  • sting.Builder拼接字符串而不是字符串直接相加拼接(Builder因为在转string时直接将byte数组转为身string而减少了一次申请数组和数据转换而比Buffer快)
  • 使用空结构体节省空间。在使用map构造set数据结构时可以用
  • atomic包替换lock在保证并发安全时提高对单一变量的访问和操作

小结:

  1. 避免常见的性能陷阱可以保证大部分程序的性能
  2. 普通应用代码,不要一味地追求程序的性能
  3. 越高级的性能优化手段越容易出现问题
  4. 在满足正确可靠、简洁清晰的质量要求的前提下提高程序性能