Golang 性能优化与编码规范 | 青训营笔记

82 阅读5分钟

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

本节课重点

本节课主要讲了 Golang 性能优化,以及编码规范。

详细知识点

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

实践例子

高质量编程

编写的代码能够达到正确可靠,简洁清晰的目标可称为高质量代码。

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

编程原则

  • 简单性

    • 消除多余的复杂性,以简单的逻辑编写代码
  • 可读性

    • 便携可维护代码的第一步是确保代码可读性
  • 生产力

    • 团队整体工作效率很重要

编码规范

注释

  • 公共符号、函数始终要注释

包中声明的每一个公共符号、变量、常量、函数以及结构体都需要注释

任何不明显也不简短的公共功能也需要给予注释

无论长度或复杂度如何,对库中的任何函数都必须进行注释

有一例外,不需要注释实现接口的方法

  • 注释应该有的作用

应该解释代码作用

应该解释代码如何做的

应该解释代码实现的原因

应该解释代码为什么会出错

小结

  • 注释应该提供未表达出来的上下文信息
  • 代码是最好的注释

代码格式

gofmt 推荐使用 go fmt 自动格式化代码

go imports 在 go fmt 的基础上加上了依赖包管理,自动增加删除依赖包应用、将依赖包按字母排序并分类

命名规范

变量命名
  • 简洁胜于冗长
  • 缩略词全部大写,但当位于变量头部并且不需要导出的时候,全部小写,比如使用 ServerHTTP 而不是 ServerHttp
  • 变量距离被使用的地方越远,则需要携带越多的上下文信息
函数命名
  • 函数名不携带包的上下文信息,因为总是成对出现
  • 函数名尽可能简短
  • 当名为 foo 的包中的某个函数返回类型 foo 时,可以省略类型信息而不导致歧义
  • 当返回类型为 T 时,则需要在函数名中加入类型信息
// Good
func Serve(I net.Listener, handler Handler) error
// bad
func ServeHTTP(I net.Listener, handler Handler) error
包名
  • 只能由小写字母组成,不包含下划线

  • 简洁,并包含一定的上下文

  • 不能与标准库同名

  • 建议满足

    • 不实用常用变量名,如使用 bufio 而不是 buf
    • 使用单数而不是复数
    • 谨慎使用缩写。例如 fmt 在不破坏上下文的情况下可以替代 format

控制流程

  • 避免嵌套,保持正常流程清晰
// Bad
if foo {
 return x
} else {
 return nil
}
​
// Good
if foo {
 return x
}
return nil
  • 尽量保持正常代码路径为最小缩进

    • 优先处理错误情况/特殊情况,尽早返回
    • 尽量避免错误嵌套
// Bad
func OneFunc() error {
 err := doSth()
 if err == nil {
   err := doASth()
   if err == nil {
     return nil
  }
   return nil
}
 return nil
}
​
// Good
func OneFunc() error {
 if err := doSth(); err != nil {
return err
}
 if err := doASth(); err != nil {
   return err
}
 return nil
}
  • 处理逻辑尽量走直线
  • 故障问题大多数出现在循环语句以及复杂的判断语句中

错误与异常处理

简单错误

  • 简单错误是指只会出现一次的错误,并且在其他地方不需要捕获该错误
  • 优先使用 errors.New 来创建匿名变量直接表示简单错误
  • 如果有格式化的要求,使用 fmt.Errorf

错误的Wrap和Unwarp

  • 错误的 Wrap 实际上是提供一个 error 嵌套另一个 error 的能力,从而能够跟踪错误链
  • 在 fmt.Errorf 中使用 %w 将一个错误关联到错误链中

错误判定

  • 判定一个错误是不是特定错误,使用 errors.ls() ,不同于使用 ==,使用该方法可以判定错误链上的所有错误是否含有特定错误
  • 在错误链上获取特定错误内容使用 errors.As
if _, err := os.Open("no_existing"); err != nil {
var pathError *fs.PathError
 if error.As(err, &pathError) {
   fmt.Println("Failed at path", pathError.Path)
} else {
   fmt.Println(err)
}
}

panic

  • 不建议在业务中使用panic
  • 调用函数不包含 recovery 会造成程序崩溃
  • 若问题可以被屏蔽或者解决,建议使用 error 代替 panic
  • 序启动的初始阶段出现不可逆转的错误时,可以在 init 或者 main 函数中使用 panic

recover

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

性能优化建议

  • 性能表现需要数据说明
  • go中提供了支持基准性能测试的benchmark工具,使用命令 go test -bench=. -benchmem

slice预分配内存

尽可能使用 make() 初始化切片的时候提供容量信息

// Bad
func NoPreAlloc(size int) {
 data := make([]int0)
 for k := 0; k <= data; k++ {
   data = append(data, k)
}
}
// Good
func NoPreAlloc(size int) {
 data := make([]int0, size)
 for k:= 0; k <= data; k++ {
   data = append(data, k)
}
}
切片原理
  • 切片本质上是一个数组片段的描述

    • 包括数组指针
    • 片段的长度
    • 片段的容量
  • 切片操作并不复制切片指向的元素

  • 创建一个新的切片会复用原来切片的底层数组

type slice struct {
 array unsafe.pointer
 len int
 cap int
}

陷阱: 大内存没有释放

在已有切片的基础上创建切片不会创建新的切片数组

原切片较大,代码在原切片的基础上新建小切片,底层数组在内存中有引用,得不到释放,因此我们可以使用 copy 代替 re-slice

func Bad(origin []int) []int {
 return origin[len(origin)-2:]
}
​
// 大幅降低内存占用
func Good(origin []int) []int {
 result := make([]int2)
 copy(result, origin[len(origin)-2:])
 return result
}

map预分配内存

  • 不断向map中添加元素会触发 map 扩容
  • 提前分配好空间可以减少内存拷贝和 Rehash 的消耗
  • 建议根据实际情况提前预估好分配的空间

字符串拼接

使用 strings.Builder 或 bytes.Buffer 拼接字符串会减少性能消耗,因为 string 类型是不可变的,每次使用 + 都会重新分配内存

// 性能最高
func StrBuilder(n int, str string) string{
 var builder strings.Builder
 for i := 0; i <= n; i++ {
   builder.WriteString(str)
}
 return builder.String()
}
​
func ByteBuffer(n int, str string) string{
 buf := new(bytes.Buffer)
 for i := 0; i <= n; i++ {
   buf.WriteString(str)
}
 return buf.String()
}

使用空结构体节省内存

空结构体并不会占据任何的内存空间,可以作为占位符使用,并且可以节省资源

map中使用这个特点,可以用来实现set,因为只需要key不需要value,会更加节省内存

使用 atomic 包

使用atomic包来替换sync.Mutex加锁可以提高性能

type atomicCounter struct {
 i int32
}
​
func AtomicAddOne(c *atomicCounter) {
 atomic.AddInt32(&c.i, 1)
}

sync.Mutex 应该用于保护一段逻辑,不仅仅用于保护一个变量

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

总结

编码中我们需要注意

  • 要降低阅读理解代码的成本
  • 重点考虑上下文信息
  • 设计简洁清晰的名称

编码时,要注意性能优化