Go高质量编程原则与Go程序性能优化 | 青训营笔记

46 阅读10分钟

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

只不过是字节给我的任务罢了

高质量编程

高质量——代码清晰可靠,简洁清晰

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

编程原则

简单性:消除多于的复杂性,以简单清晰的逻辑编写代码

可读性:代码要易读,以便维护

生产力:团队工作效率很重要

如何编写更简洁清晰的代码

  • 公共符号必须注释

    • 公共的函数、变量、常量以及结构必须注释
    • 库中提供的任何函数必须注释
    • 不需要注释实现接口的方法
  • 代码格式

    • 使用gofmt自动格式化go语言代码
    • 使用goimports自动增删依赖包
  • 注释

    • 解释代码作用
    • 解释代码如何做的(对功能不明显的复杂代码进行注释)
    • 代码实现的原因
    • 解释代码什么情况出错
    • 代码是最好的注释
    • 注释应该提供代码未表达的上下文信息
  • 命名规范

    • 变量名

      • 简洁胜于冗长
      • 缩略词全大写,如果缩略词位于开头且不需要导出时,使用全小写
      • 变量距离被使用的语句越远,则越需要携带更多的上下文信息
      • 命名变量时,尽量见名知意,而非单纯的使用单个字母
    • 函数名

      • 函数名不携带包名的上下文信息
      • 函数名尽量简短
      • 当包名和包内函数名相似时,可以不在函数名中添加类型信息
      • 如果包名和包内函数名不相似时,需要在函数名中添加类型信息
    • 包名

      • 只能由小写字母组成,不要包含大写字母和下划线

      • 不要与标准库同名

      • 尽量满足

        • 不使用常用变量名作为包名
        • 单词使用单数而非复数
        • 谨慎使用缩写
  • 控制流程

    • 避免嵌套,尽量走线性流程,保证正常流程清晰
    • 优先处理错误情况/特殊情况,尽早返回或继续循环来减少嵌套
    • 尽量保证正常代码路径为最小缩进
  • 错误和异常处理

    • 简单错误

      • 简单的错误指仅出现一次的错误
      • 优先使用error.New()创建匿名变量来直接表示简单错误
      • 如果有格式化要求,使用fmt.Errorf()
    • 错误的Wrap和Unwrap

      • 错误的Wrap实际上提供了一个error嵌套另一个error的能力,从而生成error跟踪链
      • fmt.Errorf()中使用%w关键字将一个错误关联至错误链中
    • 错误判定

      • 使用Error.Is(err, ErrorName)判断是否为特定错误
      • 使用error.As(err, &pathError)在错误链上获取特定的错误
    • panic

      • 不建议在业务代码中使用panic
      • 调用函数如果不包含recover会造成程序崩溃
      • 如果问题可以被屏蔽或解决,建议使用error代替panic
      • 当启动阶段发生不可逆转的错误时,可以在init或main函数中使用panic
    • recover

      • recover只能在被defer的函数中使用

      • 嵌套无法生效

      • 只在当前goroutine生效

      • defer语句是后进先出

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

         defer func() {
             if e := recover(); e != nil {
                 f = nil
                 err = fmt.Errorf("gitfs panic: %v\n%s", e, debug.Stack())
             }
         }()
        

Go性能优化

常用Go语言程序优化手段

Benchmark

 go test -bench=. -benchmem # 运行基准测试

slice预分配内存

尽可能在初始化时,提供初始容量信息,减少底层内存分配次数

切片的本质是一个数组片段的描述

 type slice struct {
     array unsafe.Pointer // 数组指针
     len int // 片段长度
     cap int // 片段容量(不改变内存分配的情况下的最大长度)
 }

切片操作并不复制切片指向的元素

创建一个切片会复用原来切片的底层数组

因为这一特性,可能会导致一种情况

  • 大内存未释放

    原切片较大,代码会在原切片基础上新建小切片,原底层数组在内存中有引用,得不到释放,可以使用copy()函数代替在原有基础上新建小切片

map预分配内存

尽可能在初始化时,提供初始容量信息

不断地向map添加元素可能会触发map扩容,增加内存分配次数,因此尽可能根据需求提前分配好空间,减少内存拷贝和Rehash的消耗

字符串处理

使用+拼接的性能最差,string.Builder()bytes.Buffer()性能相近,前者更快

使用+每次都会重新分配内存,而后两者的底层都是[]byte数组,使用内存扩容策略,不需要每次重新分配内存

空结构体

使用空结构体节省内存,空结构体不占据任何内存空间,可以作为各个场景下的占位符使用

 m := make(map[int]struct{})
 m[0] = struct{}{}
 ​
 m := make(map[int]bool)
 m[0] = false

实现set可以考虑使用空结构体代替map

set只需要key,不需要value,如果使用map,即时value类型为bool,也会占用1byte空间,而空结构体不占据任何内存空间

golang-set非线程安全开源实现

atomic包

锁的实现是通过操作系统实现的,属于系统调用

atomic操作是通过硬件实现,效率比锁高

sync.Mutex应该保护一段逻辑,而非一个变量

对于非数值操作,可以使用atomic.Value,能承载一个interface{}

使用atomic包代替使用性能差的sync.Mutex

 type atomicCounter struct {
     i int32
 }
 ​
 func AtomicAddOne(c *atomicCounter) {
     atomic.AddInt32(&c.i, 1)
 }

性能优化的原则和流程

  • 性能调优原则

    • 要依靠数据,而非猜测
    • 定位最主要的瓶颈,而非细枝末节
    • 不要过早优化,等到业务逻辑和功能确定后,性能出现问题后再优化
    • 不要过度优化

Go程序性能分析工具

性能分析工具pprof

  • 用于可视化和分析性能分析数据的工具

  • 可以查看CPU、Memory消耗

  • 查看CPU消耗topN

     go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10" 
     (pprof)top
    
    • flat 当前函数本身执行耗时
    • flat% flat占CPU总时间的比例
    • sum% 上面每一行flat%总和
    • cum 指当前函数本身加上其调用函数的总耗时
    • cum% cum占CPU总时间的比例
    • flat==cum函数中没有调用其他函数;flat==0函数中只有其他函数的调用
  • 根据指定到正则表达式查找代码行list re

  • 调用关系可视化web

     (pprof)web
    
    • 这个命令需要软件支持graphviz.gitlab.io/download/
    • 如果安装了上面的软件,在终端(Windows)里执行web仍然报错Failed to execute dot. Is Graphviz installed? Error: exec: "dot": executable file not found in %PATH%,以管理员打开cmd,进入软件安装路径(进入到安装路径的bin文件夹),执行dot -c,再随便打开一个cmd执行dot -version,验证是否成功,如果使用Goland的Terminal仍然报错Failed to execute dot. Is Graphviz installed? Error: exec: "dot": executable file not found in %PATH%,重启Goland即可
  • 查看堆内存Heap

     go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
    

    在弹出的页面里可以更改视图,或者使用选项卡里的更多功能,查看不同信息

    • SAMPLE

      • alloc_objects程序累计申请对象数
      • inuse_objects程序当前持有对象数
      • alloc_space程序累计申请内存大小
      • inuse_space程序当前占用内存大小
  • Goroutine

     go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"
    

    VIEW——FlameGraph可以查看火焰图

  • Mutex

     go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"
    
  • block

     go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"
    

pprof采样过程和原理

  • CPU采样

    • 采样对象函数调用和其占用的时间

    • 采样率:100次/s,固定值

    • 采样时间:从手动启动到手动结束

    • 开始采样——设定信号处理函数——开启定时器

      • 进程启动定时器
      • 操作系统:每10ms向进程发送一次SIGPROF信号
      • 进程:进程收到OS发来的SIGPROF后记录调用堆栈,然后启动写缓冲
      • 写缓冲:启动后,每100ms读取已经记录的调用栈并写入输出流
      • 进程结束定时器,结束写缓冲输出
    • 停止采样——取消信号处理函数——关闭定时器

  • Heap堆内存采样

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

    • 记录所有用户发起且在运行中的Goroutine(入口非runtime开头的)runtime.main的调用栈信息
    • Stop The World——遍历allg切片——输出创建g的堆栈——Start The World
  • ThreadCreate

    • 记录程序创建的所有系统线程的信息
    • Stop The World——遍历allm链表——输出创建m的堆栈——Start The World
  • Block阻塞和Mutex锁

    • 阻塞操作

      • 采样阻塞操作的次数和耗时
      • 采样率:阻塞耗时超过阈值的才会被记录,时间未到则丢弃,1为每次都记录
    • 锁竞争

      • 采样争抢锁的次数和耗时

        • 采样率:只记录固定比例的锁操作,比例未到则丢弃,1为每次加锁均记录

性能调优案例

业务服务优化

  • 基本概念

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

    • 建立服务性能评估手段

      • 服务性能评估方式

        • 单独的benchmark无法满足复杂逻辑分析
        • 不同负载情况下性能表现差异
      • 请求流量构造

        • 不同请求参数覆盖逻辑不同
        • 线上真实流量情况
      • 压测范围

        • 单机器压测
        • 集群压测
      • 性能数据采集

        • 单机性能数据
        • 集群性能数据
    • 分析性能数据,定位性能瓶颈

      • 使用火焰图查看高并发场景的优化问题
    • 重点优化改造

      • 正确性是基础

      • 响应数据diff

        • 线上请求数据录制回放
        • 新旧逻辑接口数据diff
    • 优化效果验证

      • 重复压测验证

      • 上线评估优化效果

        • 关注服务监控
        • 逐步放量
        • 收集性能数据
    • 进一步优化,服务整体链路分析

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

基础库优化

  • AB实验SDK的优化

    • 分析基础库核心逻辑和性能瓶颈

      • 设计完善改造方案
      • 数据按需获取
      • 数据序列化协议优化
    • 内部压测验证

    • 推广业务服务落地验证

Go语言优化

  • 编译器&运行时优化

    • 优化内存分配策略

    • 优化编译流程,生成更高效的程序

    • 内部压测验证

    • 推广业务服务落地验证

    • 优点

      • 接入简单,只需要调整编译配置
      • 通用性强

参考

www.cnblogs.com/zhzhlong/p/…

blog.csdn.net/uisoul/arti…