Go语言入门指南——高质量编程及性能调优 | 青训营

104 阅读5分钟

基于青训营学习视频以及一些资料,本文介绍了关于高质量编程的注释、命名规范、控制流程、错误和异常处理这些方面的常见编码规范以及性能分析工具pprof的使用。

Num3

高质量编程

即编写的代码能够达到正确可靠、简洁清晰的目标,可以拥有以下几个特点。

  • 正确性:是否考虑各种边界条件,错误的调用是否能够处理
  • 可靠性:异常情况或者错误的处理策略是否明确,依赖的服务出现异常是否能够处理
  • 简洁:逻辑是否简单,后续调整功能或新增功能是否能够快速支持
  • 清晰:其他人在阅读代码的时候是否能清楚明白,重构或修改功能是否不用担心出现无法预料的问题
  • 简单性:消除多余的复杂性,以简单清晰的逻辑编写代码,因为不好理解的代码无法修复改进
  • 可读性:代码是写给人看的,而不是机器,编写可维护代码的第一步是确保代码可读
  • 生产力:团队整体的工作效率非常重要,为了减低新员工上手项目代码的成本,Go语言甚至通过工具强制统一所有代码格式。

高质量的代码并不仅仅局限于哪一门语言或者哪一个工程,而应当是作为一个coder的基本素养。

编码规范

注释:

  • 注释应该解释代码作用
  • 注释应该解释代码如何做的
  • 注释应该解释代码实现的原因
  • 注释应该解释代码什么情况会出错
  • 公共符号始终要注释

命名规范:

  • 简洁胜于冗长
  • 缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写(使用ServeHTTP而不是ServeHttp
  • 变量距离其被使用的地方越远,则需要携带越多的上下文信息
  • 函数名不携带包名的上下文信息且尽量简短
  • package名只由小写字母组成

控制流程:

  • 避免嵌套,保持正常流程清晰,例如去掉不必要的else
  • 尽量保持正常代码路径为最小缩进,能对称就对称
  • 故障问题的大多出现在复杂的条件语句和循环语句中,尽量化简

错误和异常处理:

  • 简单错误:指仅出现一次的错误,且在其他地方不需要捕获该错误
  • 优先使用errors.New来创建匿名变量来直接表示简单错误
  • 如果有格式化需求,请使用fmt.Errorf
  • 错误的WrapUnnwrap:错误的Wrap实际上是提供了一个 error 嵌套另一个 error 的能力,从而生成一个 error 跟踪链,可以在fmt.Errorf中使用%w关键字来将一个错误关联至错误链中
  • 错误判定:判定一个错误是否为特定错误,用errors.ls,不同于使用==,该方法可以判定错误链上的所有错误是否含有特定的错误。在错误链上获取特定种类的错误,使用errors.As

修改优化建议

Benchmark

性能表现需要实际数量来衡量,Go语言提供了支持基准性能测试的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 来进行测试,得出测试的各项数据

Slice预分配内存

尽可能在使用make()初始化切片的时候就提供容量信息,执行时间会差很多,因为切片本质是一个数组片段的描述,包括数字指针、片段的长度以及片段的容量,切片操作并不复制切片指向的元素,并且创建一个新的切片会复用原来切片的底层数组。

Map预分配内存

与 Slice 相似,如果初始化 size 也可以很大程度上优化性能,分析其原因是:不断向map中添加元素会触发 map 的扩容,提前分配好空间可以减少内存拷贝和Rehash的消耗。

strings.Builder

在字符串拼接的过程中,使用strings.Builder往往比直接+要快,分析如下:

  • 字符串在Go语言中是不可变类型,占用内存大小是固定的
  • 使用+每次都会重新分配内存
  • strings.Builder, bytes.Buffer底层都是 []byte 数组
  • 内存扩容策略,不需要每次拼接重新分配内存
func StrBuilder(n int, str string) string {
    var builder strings.Builder
    for i := 0; i < n; i++ {
        builder.WriteString(str)
    }
    return builder.String()
}

空结构体

使用空结构体节省内存,空结构体struct{}实例不占据任何的内存空间,并且可作为各种场景下的占位符使用。

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

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

atomic包

即原子变量与原子操作。

  • atomic提供的原子操作能够确保任意时刻只有一个goroutine对变量进行操作
  • 善用atomic能够避免程序中出现大量的锁操作
  • 锁的实现是通过操作系统来实现,属于系统调用
  • atomic操作是通过硬件实现,效率显然高

atomic的常见操作:增减、载入 read、cas、交换、存储 write

var x int32 = 100
// atomic内部是一个compare ans swap, 简称cas, 会在加减操作之前先比较old new两个值再进行操作,而sync.Mutex应该用于保护一段逻辑,而不是仅仅一个变量
func f_add() {
	atomic.AddInt32(&x, 1)
}

func f_sub() {
	atomic.AddInt32(&x, -1)
}

func main() {
	for i := 0; i < 100; i++ {
		f_add()
		f_sub()
	}
	fmt.Printf("x: %v\n", x)
}

性能调优实战

性能分析工具pprof

pprof 是用于可视化和分析性能、分析数据的工具。Go 语言自带的 pprof 库就可以分析程序的运行情况,并且提供可视化的功能,它包含两个相关的库:runtime/pprof

pprof的用途

  • CPU Profiling:CPU 分析,按照一定的频率采集所监听的应用程序 CPU(含寄存器)的使用情况,可确定应用程序在主动消耗 CPU 周期时花费时间的位置
  • Memory Profiling:内存分析,在应用程序进行堆分配时记录堆栈跟踪,用于监视当前和历史内存使用情况,以及检查内存泄漏
  • Block Profiling:阻塞分析,记录goroutine阻塞等待同步(包括定时器通道)的位置。阻塞分析对分析程序并发瓶颈非常有帮助。
  • Mutex Profiling:互斥锁分析,报告互斥锁的竞争情况

所以当内存或者cpu飙升的时候,我们可以使用 go 自带的性能分析器 pprof 来查找问题所在。

利用 runtime/pprof 包实现 cpu 分析的步骤:

package main

import (
    "flag"
    "fmt"
    "log"
    "os"
    "runtime/pprof"
)

//执行 go run main -help 查看帮助信息
//执行 go run main -cpuprofile cpu.prof 生成cpu性能分析文件
func main() {
    var cpuprofile = flag.String("cpuprofile", "", "请输入 -cpuprofile 指定cpu性能分析文件名称")
    //在所有flag都注册之后,调用:flag.Parse()
    flag.Parse()
    f, err := os.Create(*cpuprofile)
    if err != nil {
        log.Fatal("could not create CPU profile: ", err)
    }
    // StartCPUProfile为当前进程开启CPU profile。
    if err := pprof.StartCPUProfile(f); err != nil {
        log.Fatal("could not start CPU profile: ", err)
    }
    // StopCPUProfile会停止当前的CPU profile(如果有)
    defer pprof.StopCPUProfile()

    sum := 0
    for i := 0; i < 100; i++ {
        sum += i
    }
    fmt.Printf("sum=%d\n", sum)
}

利用 runtime/pprof 包实现内存分析的步骤:

package main

import (
    "flag"
    "fmt"
    "log"
    "os"
    "runtime"
    "runtime/pprof"
)

//执行 go run main -help 查看帮助信息
//执行 go run main -menprofile men.prof 生成内存性能分析文件
func main() {
    var menprofile = flag.String("menprofile", "", "请输入 -menprofile 指定内存性能分析文件名称")
    //在所有flag都注册之后,调用:flag.Parse()
    flag.Parse()
    f, err := os.Create(*menprofile)
    if err != nil {
        log.Fatal("could not create memory profile: ", err)
    }
    defer f.Close() // error handling omitted for example
    runtime.GC()    // get up-to-date statistics
    if err := pprof.WriteHeapProfile(f); err != nil {
        log.Fatal("could not write memory profile: ", err)
    }
    sum := 0
    for i := 0; i < 100; i++ {
        sum += i
    }
    fmt.Printf("sum=%d\n", sum)
}

性能调优案例

业务服务优化

  • 建立服务性能评估手段
  • 分析性能数据,定位性能瓶颈
  • 重点优化项改造
  • 优化效果验证

基础库优化

  • 统计基础库使用占比
  • 分析基础库核心逻辑和性能瓶颈
  • 内部压测验证
  • 推广业务服务落地验证

go语言优化

  • 优化内存分配策略,如gc时的内存分配与回收,根据自己的业务运行统计数据来具体分析。
  • 优化代码编译流程,生成更高效的程序,函数内联,逃逸分析等。
  • 内部压测验证
  • 推广业务服务落地验证