Go 语言编码规范和性能优化| 青训营笔记

64 阅读9分钟

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

高质量编程
性能调优

1.高质量编程

1.1 高质量定义

高质量的定义——正确可靠、简洁清晰

  • 正确性:是否考虑各种边界条件,错误的调用是否能够处理
  • 可靠性:异常情况或者错误的处理策略是否明确,稳定性保证
  • 简洁:易读,逻辑是否简单,后续调整功能或新增功能是否能够快速理解结构和实现支持
  • 清晰:易维护,重构或修改功能时高效

IN GO

  • 简单性:消除多余的复杂性,以简单清晰的逻辑编写代码,不好理解的代码无法修复改进
  • 可读性:编写可维护代码的第一步是确保代码对人可读
  • 生产力:团队整体的工作效率非常重要,Go语言甚至通过工具强制统一所有代码格式,以降低后续联调/测试/验证/上线等各节点出现问题的概率

1.2 编码规范

  • 代码格式
    • gofmt/go fmt 可格式化代码
    • goimports = fmt+依赖包管理
  • 注释
    • 注释应该解释代码作用/实现过程/实现原因(外部因素-提供额外上下文)/出错场景(限制条件)
    • 公共符号始终要注释
      • 不简短不明显的变量/常量/结构
      • 所有函数
      • ex.无需注释实现接口的方法
  • 命名规范
    • varaibles
      • 简洁胜于冗长
      • 缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
      • 变量距离其被使用的地方越远,则需要携带越多的上下文信息
    • function 函数名不携带包名的上下文信息, 尽量简短;大写开头
    • package名只由小写字母组成; 使用单数,谨慎缩写
  • 控制流程
    • 线性原理,处理逻辑尽量走直线,避免嵌套,故障问题的大多出现在复杂的条件语句和循环语句中,尽量化简
    • 尽量保持正常代码路径为最小缩进,能对称就对称(从正常流程调整到先处理错误
  • 错误和异常处理
    • 简单错误:指仅出现一次的错误,且在其他地方不需要捕获该错误

      • 优先使用errors.New来创建匿名变量来直接表示简单错误
      • 如果有格式化需求,请使用fmt.Errorf
    • 错误的wrap和unwrap

      • 错误的Wrap打包提供了一个error嵌套另一个error的能力,从而生成error跟踪链

      • fmt.Errorf中使用%w关键字来将一个错误关联至错误链

        fmt.Errorf("reading srcfiles list: %w", err)
        
    • 错误判定:[error尽量提供简明的上下文信息链,方便定位问题]

      • 判定一个错误是否为特定错误,用errors.ls,不同于使用==,该方法可以判定错误链上的所有错误是否含有特定的错误

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

        • 与Is的区别是,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会导致程序崩溃

    • recover:与panic对应,如果需要更多的上下文信息可以在recover后在log中记录当前的调用栈,(应用场景:引入库的bug影响自身逻辑)

      • 只能在当前goroutine的被defer(相关语句后进先出)的函数中使用
      • 嵌套无法生效

1.3 性能优化

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

1.3.1 Benchmark

 go test -bench=. -benchmen

执行结果说明 image.png

1.3.2 slice预分配内存

尽可能在使用make()初始化切片的时候就提供容量信息

  • 切片本质是一个数组片段的描述,包括不安全的数组指针、片段的长度以及片段的容量
  • 切片操作并不复制切片指向的元素
  • 创建一个新的切片会复用原来切片的底层数组
  • 另一个陷阱:大内存未释放
    • 原切片由大量元素构成,在原切片的基础上新建小切片,原底层数组在内存有引用,得不到释放
    • 可以用copy代替re-slice

1.3.3 Map预分配内存

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

1.3.4 字符串处理 strings.Builder

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

  • 字符串在Go语言中是不可变类型,占用内存大小是固定的,使用+每次都会重新分配内存

  • strings.Builder, bytes.Buffer底层都是[]byte数组,内存扩容策略,不需要每次拼接重新分配内存,也可预分配(用Grow函数)

    var builder strings.Builder//直接将底层的[]byte数组转换成字符串类型返回,效率更高
    buf:=new(btyes.Buffer)//后续转换为字符串时重新申请了一块空间
    

1.3.5 空结构体——节省内存

  • 空结构体struct{}实例不占据任何的内存空间
  • 可作为各种场景下的占位符使用(如set实现,用map代替
    • 节省资源

1.3.6 atomic包

提供了底层的原子级内存操作,对于同步算法的实现很有用。但除了某些特殊的底层应用,使用通道或者sync包的函数/类型实现同步更好

  • atomic提供的原子操作能够确保任意时刻只有一个goroutine对变量进行操作
  • 锁的实现是通过操作系统来实现,属于系统调用
  • atomic操作是通过硬件实现,效率更高,善用atomic能够避免程序中出现大量的锁操作
  • sync.Mutex应该用于保护一段逻辑,而不是仅仅一个变量
  • 对于非数值操作,可以使用atomic.Value,能承载一个interface{}

2. 性能调优实战

2.1 原则

  • 依靠数据而非猜测
  • 找到决速反应,而非细枝末节
  • 不要过早/过度的优化
  • 保证正确性,定位主要瓶颈

2.2 性能分析工具pprof实战

2.2.1 pprof功能简介

image.png

2.2.2 pprof排查实战

搭建pprof实践项目

  • 克隆代码https://github.com/wolfogre/go-pprof-practice
  • 运行后打开http://localhost:6060/debug/pprof可以查看pprof在web上的采样数据,但是可读性很差
  • 这时可通过pprof工具来查看
    • 其采样结果是将一段时间内的信息汇总输出到文件中,首先要拿到这个profile文件
      • 可直接使用暴露的接口链接下载文件后使用
      • 也可直接用pprof工具链接这个接口下载需要的数据。

go tool pprof + <采样链接> 启动采样

go tool pprof  "http://localhost:6060/debug/pprof/profile?seconds=10" 
// 链接结尾的profile代表采样对象是CPU使用,如果直接在浏览器里打开这个链接,会启动一个60s的采样,并在结束后下载文件
//seconds=10, 采样10秒,随后采样数据就记录和下载完成,并展示出pprof终端。
//如果出现端口占用,可以通过命令 netstat -ano 查看端口占用情况
//-http://8080 参数 可直接打开pprof自带的web UI界面

pprof终端下:

  • topN 查看CPU占用最高的函数,参数说明:
    • flat 当前函数本身的执行耗时
    • cum 当前函数+其调用函数的总耗时
  • list 根据指定的正则表达式查找代码行
  • web 调用关系可视化,生成一张调用关系图,会默认使用浏览器打开,非常直观(graph视图)

定位到问题所在后可以继续修改参数为heap等处理其他方面的问题

  • heap
    • inuse
    • alloc
  • goroutine泄露易导致内存泄露
    • 可使用火焰图
    • 支持搜索(source视图下)

进阶参考 一文搞懂pprof - 知乎

2.2.3 pprof采样过程与原理

  • CPU [信号机制] image.png image.png

  • Heap [不记录栈/cgo等内存]

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

  • Block-阻塞 & Mutex-锁 image.png

3. 性能调优案例

基本概念:

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

3.1 业务服务优化

  • 流程:

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

    • 服务性能评估
    • 构造请求流量
    • 压测范围
    • 性能数据采集
  • 分析性能数据,定位性能瓶颈(pprof 火焰图)

    • 使用库不规范
    • 高并发场景优化不足
  • 重点优化项分析

    • 增加代码检查规则避免增量劣化出现
    • 优化正确性验证
    • 响应数据diff
      • 线上请求数据录制回放
      • 新旧逻辑接口数据diff
  • 优化效果验证

    • 重复压测验证
    • 上线评估
      • 关注服务监控
      • 逐步放量,避免出现问题
      • 收集性能数据
  • 进一步优化,服务整体链路分析

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

3.2 基础库优化

适应范围更广,覆盖更多服务

AB 实验 SDK 的优化

  • 分析基础库核心逻辑和性能瓶颈
    • 完善改造方案
    • 数据按需获取
    • 数据序列化协议优化
  • 内部压测验证
  • 推广业务服务落地验证

3.3 Go 语言优化

适应范围最广,Go 服务都有收益
接入简单,只需要调整编译配置,通用性强

优化方式

  • 优化内存分配策略
  • 优化代码编译流程,生成更高效的程序
  • 内部压测验证
  • 推广业务服务落地验证