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

58 阅读5分钟

这是我参与「第五届青训营」伴学笔记创作活动的第4天。青训营的第四次课程中讲解了Go语言编码规范、性能优化、pprof性能分析三个部分。下面是我个人整理的本次课程知识点梳理笔记。

编程规范

一、注释

编写注释是编写项目过程中必不可少的一部分,不仅能够提供项目使用者调用代码时的相关信息,也能帮助开发过程中个人及合作者对代码的理解。那么,什么是编写高质量注释所需要做的呢?课程中给出了以下几点提议:

  • 公共符号解释作用:当函数需要对外提供时,需要对于函数或常量等公共符号的注释,对于既不明显也不简短的公共功能,需要在函数声明之处添加代码功能的详细介绍。同时,也需要对代码的限制条件,即什么情况下会出错进行介绍。
  • 解释实现过程:当代码中出现一些比较复杂的逻辑时,需要对实现的原理进行介绍,然而对于一些简单的代码可以直接体现的流程,则不应进行注释,以防止信息冗余及迭代中注释与代码不一致的情况;
  • 解释代码原因:在编码或调试过程中,难免会对代码增加一些用于实现功能的语句,此时就应当对该种语句进行介绍,以防止代码阅读者的困惑。对于一些位置与发生功能较远的语句,则更需要对其进行注释;

二、命名规范

代码本身永远是用于理解代码最好的工具,因此代码的命名对于代码可理解性有很大的影响。

  • 变量:变量距离其所使用的地方较远时,需要携带能辨识出其含义的上下文信息,对于全局变量则需要使其在不同的地方被辨识出含义。对于如for循环中索引的小作用域变量,则不许进行冗长的解释;
  • 函数:由于包名与函数名总是成对出现,所以函数名中不应当带有包名的上下文信息,同时当函数名与其返回值类型不相同时,可以在函数名中加入返回类型信息;
  • 包名:包名应当仅由小写字母组成,且携带一定的上下文信息。需要注意包名不应当与标准库或常用变量名重名;

三、控制流程

// Good
func Func() error {
    if err := doSomeThing(); err != nil {
        return err;
    }
    if err := doAnotherThing(); err != nil {
        return err;
    }
}
// Bad
func Func() error {
    err = doSomeThing();
    if err != nil {
         err = doSomeThing();
         if err == nil {
             return nil
         }
    }
    return err
}

在代码的控制流程中,应当使得正常的流程尽可能清晰,即当错误发生时,应当优先处理错误情况,保证正常流程的统一。

四、异常处理

  • 异常打包:在返回错误时,可以使用fmt.Errorf使得一个错误关联至整体错误链中,以方便跟踪排查问题;
  • 错误判定:可以通过errors.Iserrors.As来判定错误链上是否出现某种类型的错误,以及提取对应的错误;
  • panic:在业务流程中,推荐不使用panic终结程序,如果错误可以被屏蔽,建议使用error代替panic

性能优化

在性能优化的过程中,应当结合真实的性能数据进行衡量。Go语言中提供了基准性能测试的benchmark工具,可以用于量化性能结果。在Go语言中,比较常见的几种优化性能的方法如下:

  • 预分配内存:对于slicemap结构类型,如果可以已知预计的容量,应当在初始化时定义容量信息,以避免元素扩容时的额外复杂度;
  • 释放大切片:对于slice类型而言,其实现原理为使得切片引用一个底层数组,因此当在已有切片上创建切片时,不会创建新的底层数组。因此,当函数返回切片中的小切片时,可以使用copy命令创建新的小切片,使得旧的大切片可以被释放,以节约内存。
  • 字符串处理:在字符串拼接过程中,使用+的性能较差,应当使用string.Builderbytes.Buffer进行拼接;
  • 空结构体:在Go语言中,空结构体不占用任何的内存空间,因此当实现set类型时,可以使用空结构体作为map的键类型;
  • 原子类型数据:当业务逻辑仅需对某一个数据加锁时,应当使用更轻量的atomic包,而不是使用加锁的方式;

pprof性能调优

在一个大型项目中,我们可以使用pprof函数查看各个函数的资源消耗量,以确定项目的性能瓶颈。通过pprof功能,可以可视化的显示项目运行过程中内存、CPU占用、阻塞操作、锁争用、内存分配、协程创建等资源消耗情况。

golang pprof 实战 | Wolfogre's Blog提供了一个pprof的基本使用样例,其编写了一个炸弹程序,并在其中隐藏了若干占用资源的子函数。

package main

import (
	...
	_ "net/http/pprof" // 会自动注册 handler 到 http server,方便通过 http 接口获取程序运行采样报告
        ...
)

func main() {
	runtime.GOMAXPROCS(1) // 限制 CPU 使用数,避免过载
	runtime.SetMutexProfileFraction(1) // 开启对锁调用的跟踪
	runtime.SetBlockProfileRate(1) // 开启对阻塞操作的跟踪

	go func() {
		// 启动一个 http server,注意 pprof 相关的 handler 已经自动注册过了
		if err := http.ListenAndServe(":6060", nil); err != nil {
			log.Fatal(err)
		}
		os.Exit(0)
	}()
...
    // 执行炸弹程序
}

在炸弹程序中,通过调用net/http/pprof自动注册pprof handle至HTTP服务器,并在运行炸弹程序之前,使用子协程启动HTTP服务器并监听特定地址,在这里地址为localhost::6060

image.png 在炸弹程序运行一段时间后(用于采集信号),即可访问http://localhost:6060/debug/pprof/查看具体的资源占用情况。当然,还有另外的一些可视化方法,可以参考博客,在这里不重复讲解。