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

101 阅读5分钟

这是我参与「第三届青训营-后端场」笔记创作活动的的第3篇笔记。

高质量编程

正确可靠,简洁清晰。 边界条件完备、异常处理、易读易维护。

原则:

  • 简单性
  • 可读性
  • 生产力

编码规范

  • 代码格式。使用官方工具gofmt
  • 注释。解释:
    • 代码的作用。常量、变量以及对外公开的函数等公共符号。
    • 如何做的。对复杂的逻辑进行说明。
    • 实现原因。外部原因,提供额外上下文。
    • 出错情况。解释代码的限制条件。
    • 公共符号始终要注释。

命名规范

  • 变量。
    • 缩略词全大写如ServeHTTP,但位于开头又不需要导出时全小写如xmlHTTPRequest
    • 距离使用的地方越远越详细,全局变量需要包含更多的上下文信息。
    • 函数参数命名对外提供更多信息。func send(req *Request, deadline time.Time)
  • 函数。
    • 不需要携带包名的上下文信息
    • 尽量简短
    • 当名为 foo 的包某个函数返回类型 Foo 时,可以省略类型信息而不导致歧义
    • 当名为 foo 的包某个函数返回类型 T 时(T 并不是 Foo),可以在函数名中加入类型信息
  • 包名。
    • 全部小写
    • 简短
    • 不要与标准库同名
    • 其它:不使用常用变量名;使用单数;谨慎使用缩写

控制流程

  • 避免嵌套,去除冗余。
  • 尽量保持正常代码路径为最小缩进。也就是要优先处理错误/特殊情况,尽早返回或继续循环来减少嵌套。

错误和异常处理

  • 简单错误。仅出现一次的错误,优先使用errors.New创建匿名变量直接表示。或者fmt.Errorf
  • fmt.Errorf中使用%w关键字将错误关联至错误链
    • 使用errors.Is判定错误链中是否含有特定错误
    • 使用errors.As获取错误链中特定种类的错误。
  • panic. 比错误更严重,表示程序无法正常工作。
    • 不建议在业务代码中使用,如果问题可以被屏蔽或解决,建议使用error代替。
    • 在程序启动阶段发生不可逆转错误时,可以在initmain函数中使用panic().
  • recover. 使用的其它库抛出panic时使用recover处理。
    • 只在当前goroutine中被defer的函数中生效。嵌套无法生效
    • 可以在recover之后的log中记录当前的调用栈debug.Stack()

性能优化建议

  • 使用Benchmark工具
  • Slice
    • 预分配内存。在初始化时指定容量。因为切片本质上是数组片段的描述,减少重新分配内存的次数。
    • 大内存未释放。在已有切片上创建切片不会创建新的底层数组,导致原数组得不到释放。可以使用copy代替re-slice.
  • Map
    • 预分配内存。
  • 字符串处理
    • 字符串拼接:使用+直接拼接性能最差;使用strings.Builderbytes.Buffer进行拼接性能相近,strings.Builder更快。
    • 因为使用+每次都会重新分配内存。strings.Builderbytes.Buffer有内存扩容机制,不需要每次都重新分配内存。
    • bytes.Buffer转化为字符串时重新申请了一块内存,而strings.Builder直接将[]byte类型转换成字符串类型输出。
    • 还可以利用预分配内存进一步提高性能。涉及Grow方法。
  • 空结构体
    • 不占据内存空间,可作为占位符。例如使用map[int]struct{}实现Set,可以节省资源。
  • atomic包
    • 例如atomic.AddInt32()
    • 代替互斥锁。锁的实现属于系统调用,而atomic操作通过硬件实现。
    • sync.Mutex用于保护一段逻辑,不仅仅用于保护一个变量

性能调优实战

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

使用性能分析工具 pprof. 可以知道应用在什么地方耗费了多少 CPU、memory 等运行指标。

  • 功能:工具(Tool),采样(Sa),分析(Profile),展示(View)
  • 排查CPU问题
    • go tool pprof "http://localhost:6060/debug/pprof/profile"
    • top
    • list
  • 排查堆内存问题
    • go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
    • alloc_objects/alloc_space 程序累计申请的对象/内存
    • inuse_objects/inuse_space 程序当前使用的对象/内存
  • 排查协程问题
    • go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"
    • 使用火焰图(View-Flame graph)
  • 排查锁问题
    • go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"
  • 排查阻塞问题
    • go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"
  • pprof的采样过程和原理

性能调优案例

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

业务服务优化

  • 建立服务性能评估手段
    • 服务性能评估方式。benchmark无法满足复杂逻辑分析;不同负载情况下性能差异;
    • 构造请求流量。不同请求参数覆盖的逻辑不同;线上真实流量情况(压测平台)
    • 压测范围。单机器/集群压测。
    • 性能数据采集。单机/集群性能数据。
  • 分析性能数据,定位性能瓶颈
    • pprof 火焰图定位到代码。例如使用库不规范、高并发场景优化不足
  • 重点优化项改造;
    • 优化正确性验证。线上请求数据录制回放,检查新旧逻辑响应数据diff
  • 优化效果验证
    • 重复压测验证
    • 上线评估。逐步放量,服务监控,收集数据
  • 进一步优化,服务整体链路分析(非单点服务)
    • 分析上下游业务流程,通过业务流程优化提升服务性能
    • 规范上游服务调用接口,明确场景需求

基础库优化

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

  • 分析基础库核心逻辑和性能瓶颈
  • 完善改造方案
  • 内部压测验证
  • 推广业务服务落地验证

Go语言优化

适应范围最广,Go 服务都有收益

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