Go高质量编写和性能优化 | 青训营笔记

85 阅读5分钟

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

Go高质量编写和性能优化

  • 如何编写更简洁清晰的代码
  • 常用 Go 语言程序优化手段
  • 熟悉 Go 程序性能分析工具
  • 了解工程中性能优化的原则和流程

高质量编程

什么是高质量 编写的代码能够达到正确可靠、简洁清晰的目标可称之为高质量代码

  • 各种边界条件是否考虑完备
  • 异常情况处理,稳定性保证
  • 易读易维护

编程原则

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

简单性

  • 消除“多余的复杂性”,以简单清晰的逻辑编写代码
  • 不理解的代码无法修复改进

可读性

  • 代码是写给人看的,而不是机器
  • 编写可维护代码的第一步是确保代码可读

生产力

  • 团队整体工作效率非常重要

编码规范

  • 代码格式
  • 注释
  • 命名规范
  • 控制流程
  • 错误和异常处理

注释

  • 公共符号始终要注释包中声明的每个公共的符号: 变量、常量、函数以及结构都需要添加注释
  • 任何既不明显也不简短的公共功能必须予以注释
  • 无论长度或复杂程度如何, 对库中的任何函数都必须进行注释

注释需要做什么

  • 注释应该解释代码作用
  • 注释应该解释代码如何做的
  • 注释应该解释代码实现的原因
  • 注释应该解释代码什么情况会出错

代码格式

使用goformat和goimport进行格式化

命名规范

简洁胜于冗长 缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写 变量距离其被使用的地方越远,则需要携带越多的上下文信息

控制流程

  • 避免嵌套,保持正常流程清晰
  • 尽量保持正常代码路径为最小缩进

>小结

  • 线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支
  • 正常流程代码沿着屏幕向下移动
  • 提升代码可维护性和可读性
  • 故障问题大多出现在复杂的条件语句和循环语句中

错误和异常处理

对于简单错误指的是仅出现一次的错误,优先使用errors.New如果有格式化需求,使用fmt.Errorf

错误的 Wrap 和 Unwrap

错误的 Wrap 实际上是提供了一个 error 嵌套另一个 error 的能力,从而生成一个 error 的跟踪链

fmt.Errorf 中使用:%w 关键字来将一个错误关联至错误链中

错误判定

判定一个错误是否为特定错误,使用 errors.ls

不同于使用==,使用该方法可以判定错误链上的所有错误是否含有特定的错误. if errors.Is(err,fs.ErrNotExist)

在错误链上获取特定种类的错误,使用errors.As 例如 if errors.As(err, &pathError){}

panic

  • 不建议在业务代码中使用 panic
  • 调用函数不包含 recover 会造成程序奔溃
  • 若问题可以被屏蔽或解决,建议使用 error 代替 panic
  • 当程序启动阶段发生不可逆转的错误时, 可以在 init 或 main 函数中使用 panic

recover

  • recover 只能在被 defer 的函数中使用
  • 嵌套无法生效
  • 只在当前 goroutine 生效
  • defer 的语句是后进先出
  • 如果需要更多的上下文信息,可以 recover 后在 log 中记录当前的调用栈 debug.Stack()

性能优化

Go语言提供了支持基准性能测试的benchmark工具 go test -bench=. -benchmem benchmark

  1. Slice优化 对于slice而言尽可能在使用make()初始化切片时提供容量信息,即make([]int,0,size) ->小心陷阱

在已有切片基础上创建切片,不会创建新的底层数维场景 原切片较大,代码在原切片基础上新建小切片原底层数组在内存中有引用,得不到释放 解决方法可使用 copy 替代 re-slice

  1. map预分配内存 分析
  • 不断向 map 中添加元素的操作会触发 map 的扩容
  • 提前分配好空间可以减少内存拷贝和 Rehash 的消耗
  • 建议根据实际需求提前预估好需要的空间
  1. 使用strings.Builder
  • 使用+拼接性能最差,strings.Builder,bytes.Buffer 相近,strings.Buffer 更快

分析

  • 字符串在 Go 语言中是不可变类型,占用内存大小是固定的使用 + 每次都会重新分配内存

  • strings.Builder, bytes.Buffer 底层都是 [lbyte数组

  • 内存扩容策略,不需要每次拼接重新分配内存

在已经知道字符串的长度时,可以使用 buf.Grow(len)

  1. 使用空结构体节省内存
  • 实现 Set,可以考虑用 map 来代替

  • 对于这个场景,只需要用到 map 的键,而不需要值

  • 即使是将map 的值设置为 bool 类型,也会多占据1个字节空间

一个开源实现:github.com/deckarep/go…

  1. 使用atomic包

使用 atomic 包

  • 锁的实现是通过操作系统来实现,属于系统调用
  • atomic 操作是通过硬件实现,效率比锁高
  • sync.Mutex 应该用来保护一段逻辑,不仅仅用于保护一个变量
  • 对于非数值操作,可以使用 atomic.Value,能承载一个 interface

性能优化

优化原则

  • 要依靠数据不是猜测
  • 要定位最大瓶颈而不是细枝未节
  • 不要过早优化
  • 不要过度优化

性能工具 pprof

总结

本节课主要学习了关于编码规范、错误处理等如何提高团队协作和代码维护性。其次如何通过golang自带的benchmark来测试代码效率,性能工具pprof介绍。