Chiriri's blog Chiriri's blog
首页
  • Java

    • JavaSE
    • JavaEE
    • 设计模式
  • Python

    • Python
    • Python模块
    • 机器学习
  • Golang

    • Golang
    • gRPC
  • 服务器

    • Linux
    • MySQL
    • NoSQL
    • Kubernetes
  • 项目

    • 传智健康
    • 畅购商城
  • Hadoop生态

    • Hadoop
    • Zookeeper
    • Hive
    • Flume
    • Kafka
    • Azkaban
    • Hbase
    • Scala
    • Spark
    • Flink
  • 大数据项目

    • 离线数仓
  • 青训营

    • 第四届青训营
  • HTML

    • HTML
    • JavaScript
  • Vue

    • Vue2
    • TypeScript
    • Vue3
    • Uni-APP
  • 数据结构与算法
  • C语言
  • 考研数据结构
  • 计算机组成原理
  • 计算机操作系统
  • Java基础

    • Java基础
    • Java集合
    • JUC
    • JVM
  • 框架

    • Spring
    • Dubbo
    • Spring Cloud
  • 数据库

    • MySQL
    • Redis
    • Elasticesearch
  • 消息队列

    • RabbitMQ
    • RocketMQ
  • 408

    • 计算机网络
    • 操作系统
    • 算法
  • 分类
  • 标签
  • 归档
  • 导航站
GitHub (opens new window)

Iekr

苦逼后端开发
首页
  • Java

    • JavaSE
    • JavaEE
    • 设计模式
  • Python

    • Python
    • Python模块
    • 机器学习
  • Golang

    • Golang
    • gRPC
  • 服务器

    • Linux
    • MySQL
    • NoSQL
    • Kubernetes
  • 项目

    • 传智健康
    • 畅购商城
  • Hadoop生态

    • Hadoop
    • Zookeeper
    • Hive
    • Flume
    • Kafka
    • Azkaban
    • Hbase
    • Scala
    • Spark
    • Flink
  • 大数据项目

    • 离线数仓
  • 青训营

    • 第四届青训营
  • HTML

    • HTML
    • JavaScript
  • Vue

    • Vue2
    • TypeScript
    • Vue3
    • Uni-APP
  • 数据结构与算法
  • C语言
  • 考研数据结构
  • 计算机组成原理
  • 计算机操作系统
  • Java基础

    • Java基础
    • Java集合
    • JUC
    • JVM
  • 框架

    • Spring
    • Dubbo
    • Spring Cloud
  • 数据库

    • MySQL
    • Redis
    • Elasticesearch
  • 消息队列

    • RabbitMQ
    • RocketMQ
  • 408

    • 计算机网络
    • 操作系统
    • 算法
  • 分类
  • 标签
  • 归档
  • 导航站
GitHub (opens new window)
  • JavaSE

  • JavaEE

  • Linux

  • MySQL

  • NoSQL

  • Python

  • Python模块

  • 机器学习

  • 设计模式

  • 传智健康

  • 畅购商城

  • 博客项目

  • JVM

  • JUC

  • Golang

    • Golang

      • Go 基础
      • Go 项目案例
      • Go 进阶
      • 性能调优
        • 编码规范
          • 代码格式
          • 注释
          • 注释应该解释代码作用
          • 注释应该解释代码如何做的
          • 注释应该解释代码实现的原因
          • 注释应该解释代码什么情况会出错
          • 公共符号始终要注释
          • 命名规范
          • variable
          • function
          • package
          • 总结
          • 控制流程
          • 错误和异常处理
          • 简单错误处理
          • 错误的 Wrap 和 Unwrap
          • 错误判定
          • panic
          • recover
          • 总结
        • 性能优化建议
          • Benchmark
          • Slice 预分配内存
          • Map 预分配内存
          • 使用 strings.Builder
          • 使用空结构体节省内存
          • 使用 atomic 包
          • 总结
        • 性能调用实战
          • 性能分析工具 pprof
    • gRPC

  • Kubernetes

  • 硅谷课堂

  • C

  • 源码

  • 神领物流

  • RocketMQ

  • 短链平台

  • 后端
  • Golang
  • Golang
Iekr
2023-01-21
目录

性能调优

# 性能调优

# 编码规范

什么是高质量 —— 编写的代码能的够达到正确可靠、简洁清晰的目标可称之为高质量代码

  • 各种边界条件是否考虑完备
  • 异常情况处理,稳定性保证
  • 易读易维护

编程原则

实际应用场景干变万化,各种语言的特性和语法各不相同,但是高质量编程遵循的原则是相通的

简单性

  • 消除多余的复杂性,以简单清晰的逻辑编写代码
  • 不理解的代码无法修复改进

可读性

  • 代码是写给人看的,而不是机器
  • 编写可维护代码的第一步是确保代码可读

生产力

  • 团队整体工作效率非常重要

如何编写高质量的 G0 代码

  • 代码格式
  • 注释
  • 命名规范
  • 控制流程
  • 错误和异常处理

# 代码格式

  • gofmt:Go 语言官方提供的工具,能自动格式化 Go 语言代码为官方统一风格常见 IDE 都支持方便的配置
  • goimports:Go 语言官方提供的工具,实际等于 gofmt 加上依赖包管理,自动增删依赖的包引用、将依赖包按字母序排序并分类

# 注释

# 注释应该解释代码作用

适合注释公共符号

https://github.com/golang/go/blob/master/src/os/file.go#L313

image-20230121192112564

image-20230121192138585

# 注释应该解释代码如何做的

适合注释方法

https://github.com/golang/go/blob/master/src/net/http/client.go#L678

image-20230121192241977

image-20230121192255502

# 注释应该解释代码实现的原因

解释代码的外部因素,提供额外上下文

https://github.com/golang/go/blob/master/src/net/http/client.go#L521

image-20230121192336085

# 注释应该解释代码什么情况会出错

适合解释代码的限制条件

https://github.com/golang/go/blob/master/src/time/format.go#L1344

image-20230121192415817

# 公共符号始终要注释

  • 包中声明的每个公共的符号:变量、常量、函数以及结构都需要添加注释
  • 任何既不明显也不简短的公共功能必须予以注释
  • 无论长度或复杂程度如何对库中的任何函数都必须进行注释

https://github.com/golang/go/blob/master/src/io/io.go#L638

image-20230121192740466

有一个例外,不需要注释实现接口的方法。下图图里的注释没有提供有用的信息。它没有告诉你这个方法做了什么,更糟糕是它告诉你去看其他地方的文档在这种情况下,建议完全删除该注释。

image-20230121192904222

最后选取一个 go 仓库中相对完整的代码块来说明注释

https://github.com/golang/go/blob/master/src/io/io.go#L455

首先 LimitReader 的功能有注释说明,然后是 LimitedReader 结构体的说明,就在使用它的函数之前 LimitedReader.Read 的声明遵循 LimitedReader 本身的声明,里面已经有详细说明,所以没有注释

image-20230121193022853

  • 代码是最好的注释
  • 注释应该提供代码未表达出的上下文信息

# 命名规范

# variable

  • 简洁胜于冗长
  • 缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
    • 例如使用 ServeHTTP 而不是 ServeHttp
    • 使用 XMLHTTPRequest 或者 xmlHTTPRequest
  • 变量距离其被使用的地方越远,则需要携带越多的上下文信息
    • 全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义

image-20230121193225112

i 和 index 的作用域范围仅限于 for 循环内部时,index 的额外冗长几乎没有增加对于程序的理解

image-20230121193310299

  • 将 deadli ne 替换成 t 降低了变量名的信息量
  • t 常代指任意时间
  • deadline 指截止时间,有特定的含义

# function

  • 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的
  • 函数名尽量简短
  • 当名为 foo 的包某个函数返回类型 Foo 时,可以省略类型信息而不导致歧义
  • 当名为 foo 的包某个函数返回类型 T 时(T 并不是 Foo),可以在函数名中加入类型信息

image-20230121193416005

在调用 http 包的 Server 方法时,代码是 http.Server, 携带有 http 包名,所以函数名中无需添加包信息

# package

  • 只由小写字母组成。不包含大写字母和下划线等字符
  • 简短并包含一定的上下文信息。例如 schema、task 等
  • 不要与标准库同名。例如不要使用 sync 或者 strings

以下规则尽量满足,以标准库包,名为例

  • 不使用常用变量名作为包名。例如使用 bufio 而不是 buf
  • 使用单数而不是复数。例如使用 encoding 而不是 encodings
  • 谨慎地使用缩写。例如使用 ft 在不破坏上下文的情况下比 format 更加简短

# 总结

  • 关于命名的大多数规范核心在于考虑上下文

  • 人们在阅读理解代码的时候也可以看成是计算机运行程序,好的命名能让人把关注点留在主流程上,清晰地理解程序的功能,避免频繁切换到分支细节,增加理解成本

# 控制流程

  • 避免嵌套,保持正常流程清晰 如果两个分支中都包含 return 语句,则可以去除冗余的 else

image-20230121193625170

  • 尽量保持正常代码路径为最小缩进,优先处理错误情况 / 特殊情况,并尽早返回或继续循环来减少嵌套,增加可读性

image-20230121193704854

image-20230121193721403

https://github.com/golang/go/blob/master/src/bufio/bufio.go#L277

image-20230121193810648

  • 线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支

  • 正常流程代码沿着屏幕向下移动

  • 提高代码的可读性

  • 故障问题大多出现在复杂的条件语句和循环语句中

# 错误和异常处理

# 简单错误处理

  • 简单的错误指的是仅出现一次的错误,且在其他地方不需要捕获该错误
  • 优先使用 errors.New 来创建匿名变量来直接表示简单错误
  • 如果有格式化的需求,使用 fmt.Errorf

https://github.com/golang/go/blob/master/src/net/http/client.go#L802

image-20230121193947267

# 错误的 Wrap 和 Unwrap

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

Go1.13 在 errors 中新增了三个新 API 和一个新的 format 关键字,分别是 errors.Is、errors.As 、errors.Unwrap 以及 fmt.Errorf 的 % w。如果项目运行在小于 Go1.13 的版本中,导入 golang.org/x/xerrors 来使用。以下语法均已 Go1.13 作为标准。

https://github.com/golang/go/blob/master/src/cmd/go/internal/work/exec.go#L983

image-20230121194051456

# 错误判定

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

https://github.com/golang/go/blob/master/src/cmd/go/internal/modfetch/sumdb.go#L208

image-20230121194208418

  • 在错误链上获取特定种类的错误,使用 errors.As

https://github.com/golang/go/blob/master/src/errors/wrap_test.go#L255

image-20230121194240732

# panic

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

https://github.com/Shopify/sarama/blob/main/examples/consumergroup/main.go#L94

image-20230121195611471

# recover

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

https://github.com/golang/go/blob/master/src/fmt/scan.go#L247

image-20230121195732324

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

https://github.com/golang/website/blob/master/internal/gitfs/fs.go#L228

image-20230121195802939

# 总结

  • panic 用于真正异常的情况

  • error 尽可能提供简明的上下文信息,方便定位问题

  • recover 生效范围,在当前 goroutine 的被 defer 的函数中生效

# 性能优化建议

  • 在满足正确性、可靠性、健壮性、可读性等质量因素的前提下,设法提高程序的效率
  • 性能优化是综合评估,有时候时间效率和空间效率可能对立
  • 针对 Go 语言特性,介绍 Go 相关的性能优化建议

# Benchmark

Go 语言提供了支持基准性能测试的 benchmark 工具

go test -bench=. -benchmem
1

image-20230121200147156

# Slice 预分配内存

  • 在尽可能的情况下,在使用 make () 初始化切片时提供容量信息,特别是在追加切片时

image-20230121200250285

  • 切片本质是一个数组片段的描述,包括了数组的指针,这个片段的长度和容量 (不改变内存分配情况下的最大长度)
  • 切片操作并不复制切片指向的元素,创建一个新的切片会复用原来切片的底层数组,因此切片操作是非常高效的
  • 切片有三个属性,指针 (ptr)、长度 (len) 和容量 (cap)。append 时有两种场景:
    • 当 append 之后的长度小于等于 cap,将会直接利用原底层数组剩余的空间
    • 当 append 后的长度大于 cap 时,则会分配一块更大的区域来容纳新的底层数组
  • 因此,为了避免内存发生拷贝,如果能够知道最终的切片的大小,预先设置 cap 的值能够获得最好的性能

image-20230121200347187


另一个陷阱:大内存得不到释放

  • 在已有切片的基础上进行切片,不会创建新的底层数组。因为原来的底层数组没有发生变化,内存会一直占用,直到没有变量引用该数组
  • 因此很可能出现这么一种情况,原切片由大量的元素构成,但是我们在原切片的基础上切片,虽然只使用了很小一段,但底层数组在内存中仍然占据了大量空间,得不到释放
  • 推荐的做法,使用 copy 替代 re-slice

image-20230121200545370

go test -run=. -v
1

image-20230121200601658

# Map 预分配内存

image-20230121200652372

  • 不断向 map 中添加元素的操作会触发 map 的扩容
  • 提前分配好空间可以减少内存拷贝和 Rehash 的消耗
  • 根据实际需求提前预估好需要的空间

# 使用 strings.Builder

常见的字符串拼接方式

image-20230121200757473

image-20230121200805757

  • 使用 + 拼接性能最差,strings.Builder,bytes.Buffer 相近,strings.Buffer 更快

原理

  • 字符串在 Go 语言中是不可变类型,占用内存大小是固定的,当使用 + 拼接 2 个字符串时,生成一个新的字符串,那么就需要开辟一段新的空间,新空间的大小是原来两个字符串的大小之和
  • strings.Builder,bytes.Buffer 的内存是以倍数申请的
  • strings.Builder 和 bytes.Buffer 底层都是 [] byte 数组,bytes.Buffer 转化为字符串时重新申请了一块空间,存放生成的字符串变量,而 strings.Builder 直接将底层的 [] byte 转换成了字符串类型返回

image-20230121200937815

使用 Grow 方法预分配内存

image-20230121200955133

# 使用空结构体节省内存

  • 空结构体不占据内存空间,可作为占位符使用
  • 节省资源,空结构体本身具备很强的语义,即这里不需要任何值,仅作为占位符

image-20230121201421998

  • Go 语言标准库没有提供 Set 的实现,通常使用 map 来代替。
  • 对于集合场景,只需要用到 map 的键而不需要值
  • 即使是将 map 的值设置为 bool 类型,也会多占据 1 个字节空间

一个开源实现:https://github.com/deckarep/golang-set/blob/main/threadunsafe.go

# 使用 atomic 包

image-20230121201533977

  • 锁的实现是通过操作系统来实现,属于系统调用
  • atomic 操作是通过硬件实现的,效率比锁高很多
  • sync.Mutex 应该用来保护一段逻辑,不仅仅用于保护一个变量
  • 对于非数值系列,可以使用 atomic.Value,atomic.Value 能承载一个 interface {}

# 总结

  • 避免常见的性能陷阱可以保证大部分程序的性能

  • 针对普通应用代码,不要一味地追求程序的性能,应当在满足正确可靠、简洁清晰等质量要求的前提下提高程序性能

  • 越高级的性能优化手段越容易出现问题

# 性能调用实战

性能调优原则

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

# 性能分析工具 pprof

  • pprof 是用于可视化和分析性能分析数据的工具
  • 可以知道应用在什么地方耗费了多少 CPU、memory 等运行指标

image-20230121201732038

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

编辑 (opens new window)
上次更新: 2023/12/06, 01:31:48
Go 进阶
gRPC

← Go 进阶 gRPC→

最近更新
01
k8s
06-06
02
进程与线程
03-04
03
计算机操作系统概述
02-26
更多文章>
Theme by Vdoing | Copyright © 2022-2025 Iekr | Blog
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式