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

261 阅读4分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记。

本篇笔记为第三节课 Go 语言高性能与性能调优课程的笔记。

高质量编程

高质量:正确可靠、简洁清晰

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

编程原则

  • 简单性
  • 可读性:Go 语言强制统一代码格式
  • 生产力

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

代码格式:

  • gofmt 官方工具自动格式化代码
  • goimports:gofmt+自动依赖话管理

注释:

  1. 解释代码作用:公共符号(函数)

    1. 包中声明的每个公共符号(变量、常量、函数、结构)始终要注释
  2. 解释如何做的:代码实现过程

  3. 代码实现的原因:解释代码的外部因素,提供额外上下文信息

  4. 什么情况下会出错:解释代码的限制条件

命名规范:

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

“好的命名像一个笑话,需要解释的不是好笑话。”

变量命名规范:

  1. 简洁
  2. 缩略词全大写(例:ServerHTTP);在变量开头且不需要导出时使用全小写(例:xmlHTTPRequest)
  3. 变量作用域越远需要携带越多的上下文信息(如:用 deadline 代替 t)

函数命名规范

  1. 函数名不携带包名,因为包名和函数名成对出现(http.Server
  2. 简洁
  3. 名为 foo 的包中的函数返回类型 Foo 时,可以省略类型信息;名为 foo 的包中的函数返回类型 T 时,可以省略类型信息

package命名规范

  1. 小写字母组成,不包含大写字母和上下文
  2. 简短并包含一定上下文信息(schema、task)
  3. 不要玉标准库同名(sync、string)
  4. 尽量不使用常用变量名(如用 bufio 代替 buf)
  5. 尽量使用单数(如用 encoding 而不是 encodings)
  6. 尽量谨慎使用缩写(如用 fmt 代替 format)

控制流程:

  1. 避免嵌套
  2. 优先处理错误流程,提前返回;保证正常流程的路径为最小缩进。

错误和异常处理

简单错误:仅出现一次的错误,其它地方不用捕获

  • errors.New()创建匿名变量直接表示
  • fmt.Errorf()格式化
return errors.New("Error!")

错误的 Wrap 和 Unwrap:一个 error 嵌套另一个,生成 error 的跟踪链

  • fmt.Errorf()中使用%w 来将一个错误关联至错误链中
if err != nil{
  return fmt.Errorf("Error %w!", err)
}

错误判定:

  • error.Is:判断错误链中所有错误是否包含特定错误(不同于==)
if error.Is(err, fs.ErrNotExist)
  • error.As:错误链中获取特定种类的错误
var pathError *fs.PathError
error.As(err, &pathError)
fmt.PringLn("Failed at path: ", pathError)
  • panic:程序无法正常工作

    • 不建议业务代码中使用,不包含 recover 会造成程序崩溃,能处理的使用 error
    • 启动阶段发生不可逆转的错误时,在 init 或 main 中使用 panic
  • recover:处理 panic 影响逻辑

    • 只能在当前 goroutine 的被 defer 的函数中使用,嵌套不生效
    • recover 后可以在 log 记录当前的调用栈
if e := recover(); e != nil{
  err = fmt.Errorf("panic: %v\n%s", e, debug.Stack())
}

多个 defer 的输出后进先出

Go 性能优化

  • 基准性能测试:benchmark
  1. slice 切片预分配内存
  • slice 本质:数组片段的描述,包含数组指针、片段长度、片段容量
  • 切片操作不复制切片指向的元素
data := make([]int, 0, size)
  • 原数组较大时使用 copy 代替 re-slice

    • 创建新切片复用原来切片的底层数组
    • 原数组在内存中有引用,得不到释放
  1. map 预分配内存
  • 向 map 不断添加元素会触发 map 的扩容,发生内存拷贝和 rehash
data := make(map[int]int, size)
  1. 字符串
  • 使用 strings.Builder 或 bytes.Buffer 代替直接字符串拼接(+)
  • 字符串在 Go 中不可变,+每次重新分配内存
  • strings.Builder 或 bytes.Buffer 底层都是 []byte 数组,扩容策略不需要每次重新分配内存
var builder strings.Builder
builder.WriteString(str)
return builder.String()
​
buf := new(bytes.Buffer)
buf.WriteString(str)
return buf.String()
  1. 空结构体(struct{}{})可以作为占位符,不占内存
  • 空结构体不占内存,bool 类型也会占1字节
  • 如:使用 map 来实现 Set,只用到键,用不到值
  1. atomic 包
  • atomic 是硬件实现,锁是系统调用,通过操作系统实现,因此 atomic 的效率比锁高
  • sync.Mutex 用于保护一段逻辑而不仅是变量
  • 非数值操作可以使用 atomic.Value,可以承载一个 interface{}