Go 高质量编程与性能调优 | 青训营笔记

33 阅读4分钟

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

1. 编码规范

注释

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

注释需要的内容

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

命名规范

变量

  • 简洁胜于冗长(例如在for循环中将 index 写作 i
  • 缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
    • 例如使用ServeHTTP而不是 ServeHttp
    • 使用XMLHTTPRequest 或者xmlHTTPRequest
  • 变量距离其被使用的地方越远,则需要携带越多的上下文信息
    • 全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义

函数

  • 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的(例如在 http 包中使用 ServeServeHTTP 更好)
  • 函数名尽量简短
  • 当名为 foo 的包某个函数返回类型 Foo 时,可以省略类型信息而不导致歧义
  • 当名为 foo 的包某个函数返回类型 T 时(T 并不是 Foo),可以在函数名中加入类型信息

  • 只由小写字母组成,不包含大写字母和下划线
  • 简短并包含一定的上下文信息,例如schema、task等
  • 不要与标准库同名。例如不要使用sync或者strings

以下规则尽量满足,以标准库为例

  • 不使用常用变量名作为包名。例如使用 bufio 而不是 buf
  • 使用单数而不是复数。例如使用 encoding 而不是 encodings
  • 谨慎地使用缩写。例如使用 fmt 在不破坏上下文的情况下比 format 更加简短

控制流程

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

错误和异常处理

简单错误

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

错误的 Wrap 和 Unwrap

  • 错误的Wrap 实际上是提供了一个error嵌套另一个error的能力,从而生成一个error的跟踪链
  • fmt.Errorf 中使用 : %w 关键字来将一个错误关联至错误链中,例如
err := Function(args)
if err != nil {
    return fmt.Errorf("xxx: %w", err)
}

panic

panic 是比 error 严重得多的错误,一旦出现 panic 则大概率代表程序无法继续运行,因此不建议在业务代码中使用 panic。如果问题可以被屏蔽或解决,建议使用 error 代替 panic。

但是也有例外:当程序启动阶段发生不可逆转的错误时,可以在 init 或 main 函数中使用 panic。

recover

  • recover 只能在被 defer 的函数中使用
  • 嵌套无法生效
  • 只在当前 goroutine 生效
  • defer 的语句是后进先出

2. 性能调优指南

Benchmark

使用 Benchmark 进行性能基准测试

slice

初始化 slice 时尽可能提供 cap 信息

// bad
data := make([]int, 0)
// good
data := make([]int, 0, cap)

map

初始化 map 时尽可能预分配内存

// bad
data := make(map[int]int)
// good
data := make(map[int]int, size)

字符串拼接

使用 strings.Builder

// bad
s := ""
s += str
// good
// 初始化 strings.Builder 类型字符串
var builder strings.Builder
// 字符串拼接
builder.WriteString(str)
// 将 strings.Builder 类型转换为 string 类型
builder.String()

根本原因在于 Go 语言中 string 是不可变类型,占用的内存大小是固定的,因此使用 + 拼接时会重新分配内存。

strings.Builder 底层是 []byte 数组,在拼接时只是进行了数组扩容,因此不需要重新分配内存。

如果预先知道了最终字符串的长度,则可以使用 Grow() 预分配内存进一步提高运行速度,代码如下

var builder strings.Builder
// 预分配内存
builder.Grow(length)
builder.WriteString(str)
builder.String()

使用空结构体

使用空结构体 struct{} 可以节省内存,因为 struct{} 不占用任何空间,可作为占位符使用。

m := make(map[int]struct{})
m[0] = struct{}{}

如果不使用 map 的值,那么此时 map 可以看做 set 数据结构。

使用 atomic 包

使用 atomic 包以以下代码为例

type atomicCounter struct {
    i int32
}

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

3. 性能优化分析工具

使用性能分析工具 pprof 比较重要的分析方面有

  • CPU
  • 堆内存-Heap
  • 协程-Goroutine