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

113 阅读5分钟

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

前言

本文主要讲解高质量编程的规范和性能调优分析工具pprof的使用

高质量编程

1.定义

编写的代码能够达到正确可靠、简洁清晰的目标。

  • 完整性:各种边界条件是否考虑完善
  • 稳定性:异常情况能进行处理
  • 简单性:逻辑是否清晰
  • 可读性:易懂易维护
  • 生产力:团队统一标准,整体工作效率提升

不局限于go语言,每种语言开发都应遵循以上原则。

2.编码规范

主要从代码格式、注释、命名规范、控制流程、错误和异常处理这五个部分进行讲解。

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

注释:解释代码作用、做法(如何做的)、实现原因和出错情况(限制条件),其中公共符号始终要注释。

命名规范

  • 简洁大于冗余
  • 缩略词全大写,但当位于变量开头且不需要导出时,采用小写 (使用ServeHTTP而不是ServeHttp;使用XMLHTTPRequest而不是xmlHTTPRequest)
  • 变量名称需要携带更多上下文信息
  • 函数不携带包名的上下文信息且尽量简短
  • package只由小写字母组成,且不与标准库同名

控制流程

  • 避免嵌套,保持正常流程清晰
  • 尽量保持正常代码路径为最小缩进Tab,方便检查
  • 尽量简化复杂的条件语句和循环语句,避免故障出现

错误和异常处理

  • 简单错误:只出现一次的错误,且在其他地方不需要捕获的错误,优先使用errors.Now,若有格式化需求,则用fmt.Errorf

image.png

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

image.png

  • 错误判定:判定错误是否为特定错误,采用error.ls,该方法可判定错误脸上的所有错误是否含有特定的错误。

image.png

当在错误链上获取特定种类的错误时,采用error.As

image.png

  • panic:比error更严重,在业务代码上不建议使用
  • recover:只能在defer的函数中使用;嵌套无法生效;只在当前goroutine生效;defer是一个栈,后进先出

3.性能测试建议

在满足正确可靠、简洁清晰等质量因素的条件下,进行综合评估,有时候时间效率和空间效率可能对立

Benchmark

性能表现需要实际数据衡量,采用Benchmark性能测试工具,下面是一个例子:

// from fib.go
func Fib(n int) int {
    if n < 2 {
        return n
    }
    return Fib(n - 1) + Fib(n - 2)
}

// from fib_test.go
func BenchmarkFib10(b *testing.B) {
    // run the Fib funciton b.N times
    for n := 0; n < b.N; n++ {
        Fib(10
    }
}

采用go test -bench=, -benchmen 进行测试,结果如下:

image.png

Slice预分配内存

  • 尽量在make()初始化时提供容量信息,执行时间大大减少

原因:切片本质是一个数组片段的描述,包括数字指针、片段长度和片段容量,其并不复制切片指向的元素,而是创建一个新切片来复用原来切片的底层数组

  • 另一陷阱:大内存未释放

当原切片较大,代码在原切片基础上新建小切片,这导致原底层数组在内存中有引用进而得不到释放。

解决办法:可用copy替代re-slice

map预分配内存

和slice相似,make(map[int]int,size),加上size也会提高性能

由于不断向 map 中添加元素的操作会触发 map 的扩容,因此提前分配好空间可以减少内存拷贝和 Rehash 的消耗,并建议根据实际需求提前预估好需要的空间。

strings.Builder

字符串拼接的方法,比+更快,例子如下:

func StrBuilder(n int, str string) string {
    var builder strings.Builder
    for i := 0; i < n; i++ {
        builder.WriteString(str)
    }
    return builder.String()
}

分析:由于字符串在 Go 语言中是不可变类型,占用内存大小是固定的。因此,使用+每次都会重新分配内存strings.Builder底层是 []byte 数组内存扩容策略,不需要每次拼接重新分配内存。

空结构体

节省内存,可作为各种场景的占位符来使用

func EmptyStructMap(n int) {
    m := make(map[int]struct{})
    for i := 0; i < n; i++ {
        m[i] = struct{}{}
    }
}

atomic包

由于锁的实现是通过操作系统来实现,属于系统调用。而atomic操作是通过硬件实现,效率比锁高。

常见操作:add、sub、read、wirte等

性能调优

保证正确性,定位主要瓶颈

1.原则

  • 依靠数据而不是猜测
  • 定位最大问题而不是小点
  • 不要过早和过度优化

2.性能调优工具——pprof

pprof是用于可视化、分析性能和数据的工具

image.png

常见分析对象:cpu、heap、goroutine、mutex(锁)、block

3.性能调优方法

主要从下面三方面进行性能调优:

  • 业务服务优化
  • 基础库优化(例如,AB实验SDK的优化)
  • Go语言优化(例如,编译器和运行时的优化)

业务服务优化

流程:

  • 建立服务性能评估手段
  • 分析性能数据,定位性能瓶颈(例如,使用库不规范、高并发场景优化不足等)
  • 重点优化项改造
  • 效果验证
  • 进一步优化,服务整体链路分析

小结

通过了解高质量编程,这让我更加重视代码命名、注释等的规范,并收获了一些代码逻辑技巧。此外,通过学习性能调优工具pprof,这让我们更加理解了代码的构造与运行,进而学习到了代码在编写后优化的方向。

参考

  • 字节跳动青训营高质量编程与性能调优实战教程