Go性能调优 | 青训营笔记

91 阅读5分钟

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

课程重点

  1. 高质量编程
  2. 性能优化

高质量编程

高质量的代码应该要做到 —— 正确可靠,简明清晰

Go语言编程原则:

简单性

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

可读性

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

生产力

  • 团队整体工作效率非常重要
  • 考虑完整的边界条件
  • 具有完备的异常情况处理

为了编写高质量代码,我们需要了解并遵守Go语言的编码规范:

  1. 代码格式

正确的代码格式可以让阅读这段代码的人快速了解代码的逻辑和内容

在Go语言中,官方提供了gofmt工具来快速格式化代码

在大多数常见的编辑器里,你只需要引入fmt包然后保存代码,代码就会自动格式化了

goimport也是一个很有用的工具

他不但可以格式化代码,还可以根据代码内容自动增加 / 删除依赖包

  1. 注释

注释会让我们快速的了解一段代码,一个好的注释应该做到:

  • 解释代码的作用
  • 解释代码是如何工作的
  • 解释代码实现的原因(为何这么做)
  • 解释代码何时会出错

好的代码有很多注释,坏的代码需要很多注释

  1. 命名规范

好的命名可以帮助其他人了解这个变量(函数)是做什么的

  • 简洁胜于冗长 !!
  • 缩略词全大写,但当其位于变量开头且不需要导出时全小写
  • 变量距离使用它的地方较远时需要携带上下文信息(或者使用注释?)
  1. 函数名一般不带有包的信息,因为调用时都是同步出现的
  2. 当名为 foo 的包某个函数返回类型是Foo时,可以省略类型信息而不导致歧义
  3. 包名只由小写字母组成,携带部分上下文信息且不与标准库重名
  1. 控制流程

在使用判断循环语句时尽量避免嵌套(一大堆if else套在一起是程序和人的双重煎熬)

如果必须嵌套的话,也要尽量减少嵌套

简而言之,好的流程控制就是让代码一直在最小缩进位置走直线

  1. 错误 & 异常处理
  • 简单错误

只出现一次,其他地方不需要捕获的错误

可以使用errors.New("An Error")方法返回一个匿名错误

可以使用fmt.Errorf("Error list: %w",err)函数对错误进行格式化

使用%w将错误关联起来,形成错误链

  • 错误判断

可以使用errors.ls判断错误链中是否含有指定错误,使用errors.As读取特定错误

  • panic

当程序出现严重错误无法继续运行时可以使用panic

  1. 不建议在业务代码中使用panic,如果函数中中不含recovery会导致程序崩溃
  2. 如果问题可以逆转或者修复,建议使用error而不是panic
  3. 可以在启动流程中当发送不可逆转的错误时使用panic
  • recovery

recovery对应panic是一种恢复机制,可在recovery中记录当前调用栈

  1. recovery只能在被 defer的函数中使用且嵌套无法生效
  2. recovery只对当前gorountine生效
  3. defer是后进先出

性能优化

  1. Slice(切片)
  • 内存预分配

在使用Slice切片时,我们应当尽量规定内存容量大小,避免后续因为容量不足重新分配内存

data := make(int,2)        //未规定容量
data := make(int,2,size)   //规定了容量
  • 大内存释放

当我们得到一个较大的切片但是只需要其中的一部分时,如果使用:

new_data := origin[len(origin)-2:]

那么此时原切片由于被引用,内存空间得不到释放

使用copy函数进行替代即可释放原切片占用的内存空间

new_data := make([]int, len(origin)-2)
copy(new_data,origin[len(origin)-2:])  //使用copy函数替换re-slice
  1. Map
  • 内存预分配

同Slice一样,使用Map时也应当尽量规定大小,避免扩容

map := make(map[int]int)        //未规定容量
map := make(map[int]int,size)   //规定了容量
  1. 字符串处理
  • 使用strings.Builder

在Go语言中,当我们使用+拼接字符串时,会重新分配新内存,较为费时

而使用strings.Builder或者bytes.Buffer只是进行内存拓展

所以,使用strings.Bulider会比直接使用+省时

var builder strings.Builder
builder.WriteString(str1)
builder.WriteString(str2)
str3 = builder.String()   //str3 = str1 + str2
  1. 使用空结构体节省内存

空结构体 struct{} 实例不占据任何的内存空间

可作为各种场景下的占位符使用

  • 节省资源

  • 空结构体本身具备很强的语义,即这里不需要任何值,仅作为占位符

  1. atomic包

当我们需要通过多线程完成任务,并且需要完成计数时,使用Lock通常是我们的选择

锁的实现是通过操作系统来实现,属于系统调用,调用成本高

而atomic 操作是通过硬件实现,效率比锁高

Lock 应该用来保护一段逻辑,不仅仅用于保护一个变量

var a int32 = 0
atomic.AddInt32(a,1)   //利用atomic实现+1

性能优化固然好,但也不能一味的优化

避免常见的性能陷阱即可保证大部分程序的性能,越高级的性能优化手段越容易出现问题

在满足正确可靠、简洁清晰的质量要求的前提下再去提高程序性能