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

529 阅读8分钟

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

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

零、前言:

本节主要简要介绍了高质量编程的定义和原则,

分享了代码格式、注释、命名规范、控制流程、错误和异常处理五方面的常见编码规范。

本文记录和整理了本人在跟随字节青训营学习的一些我个人感觉比较重要的内容和知识,也有一部分内容是我认为自己比较难理解或记忆的,也一并记录于此文。 撰写本文的目的主要是方便我自己的复习和查阅,倘若各位读者有与我相似的问题,也可以参考之,如果对各位有帮助那就是我莫大的荣幸,也期望各位不吝赐教,多多指出我的问题,可以在下方留言或者私信我。


通过此次课程可以学到:

  • 如何编写更简洁清晰的代码
  • 常用Go语言程序优化手段
  • 熟悉Go程序性能分析工具
  • 了解工程中性能优化的原则流程

一、本堂课重点内容:

  • 高质量编程

    • 高质量编程简介
    • 编码规范
    • 性能优化建议
  • 性能调优实战

    • 性能调优简介
    • 性能分析工具
    • pprof实战
    • 性能调优案例

二、详细知识点介绍

1. 高质量编程

高质量编程简介

高质量的定义:

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

  • 正确性:是否考虑各种边界条件,错误的调用是否能够处理
  • 可靠性:异常情况或者错误的处理策略是否明确,依赖的服务出现异常是否能够处理
  • 简洁:逻辑是否简单,后续调整功能或新增功能是否能够快速支持
  • 清晰:其他人在阅读代码的时候是否能清楚明白,重构或修改功能是否不用担心出现无法预料的问题

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

  • 简单性:消除多余的复杂性,以简单清晰的逻辑编写代码,因为不好理解的代码无法修复改进
  • 可读性:代码是写给人看的,而不是机器,编写可维护代码的第一步是确保代码可读
  • 生产力:团队整体的工作效率非常重要,为了减低新员工上手项目代码的成本,Go语言甚至通过工具强制统一所有代码格式。

编码规范

如何编写高质量的Go代码?需要注意:

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

  • 注释:Good code has lots of comments, bad code requires lots of comments.

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

    • 简洁胜于冗长

    • 缩略词全大写,但当期位于变量开头且不需要导出时,使用全小写

      • 使用ServeHTTP而不是ServeHttp
      • 使用XMLHTTPRequest而不是xmlHTTPRequest
    • 变量距离其被使用的地方越远,则需要携带越多的上下文信息

    • 函数名不携带包名的上下文信息且尽量简短

    • package名只由小写字母组成

  • 控制流程:

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

    • 简单错误:指仅出现一次的错误,且在其他地方不需要捕获该错误

    • 优先使用errors.New来创建匿名变量来直接表示简单错误

    • 如果有格式化需求,请使用fmt.Errorf

    • // 一个例子
      func defaultCheckRedirect(req *Request, via []*Request) error {
          if len(via) >= 10 {
              // 使用errors.New
              return errors.New("stopped after 10 redirects.")
          }
          return nil  // 去掉不必要的else
      }
      
    • 错误的WrapUnnwrap

      • 错误的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.ls,不同于使用==,该方法可以判定错误链上的所有错误是否含有特定的错误

        // 一个例子
        data, err = lockedfile.Read(targ)
        if errors.Is(err, fs.ErrNotExist) {
            return []byte{}, nil
        }
        return data, err
        
      • 在错误链上获取特定种类的错误,使用errors.As

        // 一个例子
        if _, err := os.Open("non-existing"); err != nil {
            var pathError *fs.PathError
            if errors.As(err, &pathError) {
                fmt.Println("Failed at path:", pathError.Path)
            } else {
                fmt.Println(err)
            }
        }
        
    • panic:比错误更严重,表示程序无法正常工作,在业务代码中不建议使用,故不展开介绍。

    • recover:与panic对应,如果需要更多的上下文信息可以在recover后在log中记录当前的调用栈

      生效条件:

      • 只能在被defer的函数中使
      • 嵌套无法生效
      • 只在当前goroutine生效
      • 注意defer是一个栈

性能优化建议

简介:

  • 性能优化的前提是满足正确可靠、简洁清晰等质量因素
  • 性能优化是综合评估,有时候时间效率和空间效率可能对立,所谓时间换空间空间换时间

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来进行测试

运行结果说明:

image.png

Slice预分配内存

尽可能在使用make()初始化切片的时候就提供容量信息,执行时间会差很多

究其原因是因为

  • 切片本质是一个数组片段的描述包括数字指针、片段的长度以及片段的容量
  • 切片操作并不复制切片指向的元素
  • 创建一个新的切片会复用原来切片的底层数组
image.png

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

有一种情况,原切片由大量元素构成,但是我们在原切片的基础上切片,虽然只使用了很小一段,但底层数组在内存中仍然占据了大量的空间,得不到释放。

这个时候我们可以用copy代替re-slice

Map预分配内存

与Slice相似地,如果初始化size也可以很大程度上优化性能,分析如下:

  • 不断向map中添加元素会触发map的扩容
  • 提前分配好空间可以减少内存拷贝和Rehash的消耗

strings.Builder

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

  • 字符串在Go语言中是不可变类型,占用内存大小是固定的
  • 使用+每次都会重新分配内存
  • strings.Builder, bytes.Buffer底层都是[]byte数组
  • 内存扩容策略,不需要每次拼接重新分配内存
// 一个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()
}

空结构体

使用空结构体节省内存,分析如下:

  • 空结构体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)
}
// 对于非数值操作,可以使用atomic.Value,能承载一个interface{}

2. 性能调优实战

性能调优简介

性能调优原则:

  • 依靠数据而非猜测
  • 找到决速反应,而非细枝末节
  • 不要过早和过度的优化

性能分析工具pprof实战

说明:

  • 我们希望知道应用在什么地方耗费了多少CPU/Memory
  • pprof是用于可视化分析性能、分析数据的工具

接下来的内容包括:

  • pprof功能简介
  • pprof排查实战
  • pprof的采样过程和原理

pprof功能简介

image.png

pprof排查实战,搭建pprof实践项目

例子:https://github.com/wolfogre/go-pprof-practice

运行后打开http://localhost:6060/debug/pprof可以查看数据,但是可读性很差

采样数据说明:

  • allocs:内存分配情况
  • blocks:阻塞操作情况
  • cmdline:程序启动命令及
  • goroutine:当前所有goroutine的堆栈信息
  • heap:堆上内存使用情况(同alloc)
  • mutex:锁竞争操作情况
  • profile: CPU占用情况
  • threadcreate:当前所有创建的系统线程的堆栈信息
  • trace:程序运行跟踪信息

这时候我们就需要通过pprof工具来帮忙查看了,其采样结果是将一段时间内的信息汇总输出到文件中,所以首先需要拿到这个profile文件,可以直接使用暴露的接口链接下载文件后使用,也可以直接用pprof工具链接这个接口下载需要的数据。

terminal中输入命令go tool pprof + <采样链接>来启动采样

例如:go tool pprof -http=:8083 "http://localhost:6060/debug/pprof/profile?seconds=10""

链接结尾的profile代表采样的对象是CPU使用,如果直接在浏览器里打开这个链接,会启动一个60s的采样,并在结束后下载文件,这里加上seconds=10来让其采样10秒,随后采样数据就记录和下载完成了,并展示出pprof终端。

值得一提的是,如果出现端口占用,可以输入netstat -ano来查看端口占用情况

其他的一些内容:

  • 输入top可以查看CPU占用最高的函数,参数说明:

image.png

  • 如果只需要查看最高的N个函数,则输入topN即可

  • 命令list根据指定的正则表达式查找代码行,例如list Eat

  • 命令web调用关系可视化,生成一张调用关系图,会默认使用浏览器打开,非常直观

    avatar

这个时候,虽然CPU已经降下来了,但是内存依然很高,其实是堆内存很高。

再次启动pprof工具,这时候链接的结尾是heap,等待采样完成后,浏览器会自动按打开,还是会展示出web视图,同时,展示的资源使用从CPU时间变成了内存占用。

把问题代码注释掉之后,性能明显提高了。

更加细节的问题请参考一文搞懂pprof - 知乎 (zhihu.com)

pprof采样过程与原理

CPU:

avatar

avatar

Heap:

  • 采样程序通过内存分配器在堆上分配和释放的内存,记录分配/释放的大小和数量
  • 采样率:每分配512KB记录一次,可在运行开头修改,1为每次分配均记录
  • 采样时间:从程序运行开始到采样时
  • 采样指标:alloc_space, alloc_objects, inuse_space, inuse_objects
  • 计算方式:inuse = alloc - free

Goroutine-协程 & ThreadCreate-线程创建

avatar

Block-阻塞 & Mutex-锁

avatar

性能调优案例

简介:

介绍实际业务服务性能优化的案例;

对逻辑相对复杂的程序如何进行性能调优;

主要分为以下三个方面:

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

业务服务优化

基本概念:

  • 服务:能单独部署,承载一定功能的程序
  • 依赖:Service A的功能实现依赖,Service B的响应结果,称为Service A依赖Service B
  • 调用链路:能支持一个接口请求的相关服务集合及其相互之间的依赖关系
  • 基础库:公共的工具包、中间件

流程:

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

avatar


未完待续...(随时补充修改)

谢谢大家的阅读,欢迎互动,也欢迎访问我的博客!

conqueror712.github.io/

1067994.jpg