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

62 阅读4分钟

高质量编程

编写的代码需要达到正确可靠、简洁清晰

  • 各种边界条件考虑完备
  • 异常情况处理
  • 易读易维护

编码规范

代码格式

gofmt自动格式化,常见IDE都支持

goimports自动增删依赖的包引用

注释

公共符号始终要注释,包中每个公共符号需要添加注释,库中的任何函数都要注释,不需要注释实现接口的方法

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

命名规范

变量命名:

  • 简介
  • 缩略词全大写,但当其位于变量开头且不需要导出使用全小写
  • 变量距离被使用的地方越远,需要携带越多的上下文信息
  • 全局变量在其名字中需要更多的上下文信息

例如

for i := 1; i < n; i++{
//...
}//这里i只在循环内使用,需要尽量简单

func (c *Client) send(req *Request, deadline time.Time)	//函数需要交给外部使用,所以变量名要详细

函数

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

包命名

  • 只由小写字母组成
  • 简短并且包含一定的上下文信息
  • 不要与标准库同名
  • 不使用常用变量名作为包名
  • 使用单数而不是复数
  • 谨慎地使用缩写

控制流程

避免嵌套,保持正常的流程信息

尽量保持正常代码路径为最小缩进,即优先处理错误以尽早继续循环来减少嵌套

错误和异常处理

简单错误:

  • 简单错误是指仅出现一次,且在其它地方不需要捕获的错误
  • 优先使用errors.New创建匿名错误
  • 可以使用fmt.Errorf

错误的Wrap和Unwrap

  • 错误的wrap实际上提供了一个error嵌套另一个error的能力,从而生成一个error的跟踪链

  • 在fmt.Errorf中使用 %w关键字来讲一个错误关联到错误链中,例如:

    list, _, err := c.GetBytes(cache.Subkey(a.actionID, "srcfiles"))
    if err != nil{
    	return fmt.Errorf("reading srcfiles list: %w", err)
    }
    

错误判定:

  • 判定一个错误是否为特定错误,使用errors.Is,例如:

    if errors.Is(err, fs.ErrNotExist){
    	//...
    }
    
  • 在错误链上获取某种特定种类的错误,使用errors.As

panic指出现了严重的错误,不建议在业务代码中使用,调用函数如果不包含revover会造成程序崩溃。

性能优化建议

Go语言提供了支持基准性能测试的benchmark工具,只需要在函数命名前缀Benchmark,然后执行

go test -bench=. -benchmem

可以得到执行时间,内存消耗等

Slice预分配内存

Slice时go语言中常用的结构,我们尽可能用make()初始化切片时提供容量信息,这样可以避免多次分配内存。

切片在底层实际上是数组片段的描述,包含一个数组指针、切片目前的长度和容量,切片的操作并不复制切片指向的元素,创建一个新的切片会复用原来的指针数组

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

如果在已有的切片基础上创建切片,不会创建新的底层数组

例如原切片较大,代码在原切片的基础上创建小切片,此时由于原底层数组在内存中仍被引用,无法得到释放,例如

func GetLastBySlice(origin []int)[] int{
	return origin[len(origin)-2:]
}

这里返回了原切片上的部分切片,因此原切片的大量内存无法释放

可以使用copy来替代re-slice:

func GetLastByCopy(origin []int)[] int{
	result := make([]int, 2)
	copy(result, origin[len(origin)-2]:)
	return result
}

map预分配内存

map同样也可以采用预分配的方式分配内存,原理和slice相似,可以根据实际需求提前预估所需要的空间。

字符串处理

建议使用strings.Builder来处理字符串的操作,在go语言中字符串是不可变的,因此更新字符串会构建新的字符串,而Builder的底层是byte数组,因此不会重新分配内存。

空结构体

使用空结构体可以节省内存,因为空结构体struct{}实际上不占用任何的空间,可以作为占位符使用,也可以用来实现set

使用atomic包

atomic是一个针对多线程的包,可以维护原子变量,相对于sync包,atomic包的性能更高。

性能调优实战

性能调优的一些原则

  • 依靠数据而不是猜测
  • 要定位最大瓶颈而不是细枝末节
  • 不要过早优化
  • 不要过度优化

性能分析工具pprof

pprof是用于可视化和分析性能分析数据的工具,其功能包括

  • 采样,采样CPU、堆内存、协程、锁、阻塞、线程创建等信息
  • 展示,支持多种展示类型
  • 工具:runtime/http
  • 分析,网页/可视化终端

在课程配套代码中展示了pprof的使用:

package main

import (
	"log"
	"net/http"
	_ "net/http/pprof"
	"os"
	"runtime"
	"time"

	"github.com/wolfogre/go-pprof-practice/animal"
)

func main() {
	log.SetFlags(log.Lshortfile | log.LstdFlags)
	log.SetOutput(os.Stdout)

	runtime.GOMAXPROCS(1)              //设置CPU使用数
	runtime.SetMutexProfileFraction(1) //跟踪锁调用
	runtime.SetBlockProfileRate(1)     //开启阻塞调用跟踪

	go func() {
		if err := http.ListenAndServe(":6060", nil); err != nil { //启动http6060端口,观测性能问题
			log.Fatal(err)
		}
		os.Exit(0)
	}()

	for {
		for _, v := range animal.AllAnimals {
			v.Live()
		}
		time.Sleep(time.Second)
	}
}

运行之后打开下面的地址就可以查看一些指标

ip:port/debug/pprof

还可以在终端中使用

go tool pprof [addr]

进入pprof命令终端,例如

go tool pprof "http://ip:port/debug/pprof/profile?second=10" 

这里采样10s的数据,可以查看CPU的信息,显示如下:

Saved profile in /root/pprof/pprof.main.samples.cpu.001.pb.gz
File: main
Type: cpu
Time: Jan 17, 2023 at 2:45pm (CST)
Duration: 30.03s, Total samples = 16.43s (54.71%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top10
Showing nodes accounting for 16.38s, 99.70% of 16.43s total
Dropped 15 nodes (cum <= 0.08s)
      flat  flat%   sum%        cum   cum%
    16.38s 99.70% 99.70%     16.38s 99.70%  github.com/wolfogre/go-pprof-practice/animal/felidae/tiger.(*Tiger).Eat
         0     0% 99.70%     16.38s 99.70%  github.com/wolfogre/go-pprof-practice/animal/felidae/tiger.(*Tiger).Live
         0     0% 99.70%     16.39s 99.76%  main.main
         0     0% 99.70%     16.39s 99.76%  runtime.main

这里列出了时间最多的函数,发现tiger.eat这个函数耗时最多。这里flat指函数本身时间,cum指的是函数本身加上其调用函数的总耗时。

我们找到耗时最多的函数:


func (t *Tiger) Eat() {
	log.Println(t.Name(), "eat")
	loop := 10000000000
	for i := 0; i < loop; i++ {
		// do nothing
	}
}

tiger的eat多了一个for循环,可以在命令中用list eat来找到,同时,使用web命令可以在浏览器中打开一个关系图。可以通过8080端口观察可视化的性能分析

go tool pprof -http=:8080"http://ip:port/debug/pprof/profile"

除了CPU之外,还可以查看内存使用、内存申请次数、协程的申请次数,阻塞情况等来排查问题,提高程序性能

采样过程和原理

CPU

CPU的采样对象是函数调用和它们占用的时间

采样的原理是利用了系统的定时信号处理,当采样开始时设定定时器和信号处理,当收到信号之后记录一次程序的调用栈,并且定时将调用栈写入输出流。

堆内存

采样程序通过内存分配器在堆上分配和释放的内存记录堆内存的分配情况,每512KB记录一次,记录分配分配的空间和对象的数量等。

协程和线程创建

记录所有用户发起且运行中的调用栈信息以及程序创建的所有的系统线程的信息。

阻塞和锁

阻塞操作采样阻塞操作的次数和耗时,只有超过阈值的阻塞才会被记录,而锁只记录固定比例的锁操作。

性能调优案例

对于比较复杂的业务,性能优化的意义很大,在实际的业务中主要分为

  • 业务服务优化
  • 基础库优化
  • Go语言优化

业务服务优化

业务服务优化的流程是:

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

评估手段:

单独的benchmark无法满足复杂的逻辑分析,不同负载、不同的请求参数下性能表现都不同,所有我们要尽量模拟和覆盖真是的线上流量情况。需要在单机器和集群上进行压测,采集单机和集群的性能数据

定位性能瓶颈:

  • 库使用不规范
  • 高并发场景优化不足:对比高峰期和低峰期的性能数据

重点优化项改造:

性能优化必须要保证正确性,需要记录下服务的数据,比较优化前后的服务差异,保证正确性不受影响。

优化效果验证:

重复进行压测验证,上线评估优化效果

进一步优化,服务整体链路分析:

  • 规范上游服务调用接口,明确场景需求
  • 分析掉哟个链路,通过业务流程优化提升服务性能

基础库优化

基础库需要被多个服务引用。需要分析基础库的核心逻辑和在不同服务下的性能瓶颈,分析具体的改进思路。在完成改进之后进行内部的压测和实际业务场景下的验证。

Go语言优化

  • 优化内存分配策略
  • 优化编译流程,生成更高效的程序