高质量编程与性能调优实战 | 青训营;

163 阅读12分钟

高质量编程与性能调优实战

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

1.1 高质量代码

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

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

1.2 开发原则

  1. 遵循Go的命名规范:按照Go语言的命名规范,使用有意义的变量、函数和方法名称,避免使用缩写或单个字母作为名称。
  2. 使用短小的函数和方法:拆分较大的函数和方法为小型的、单一的功能单元。这样做有助于提高代码的可读性,并使得代码更易于测试和维护。
  3. 减少重复代码:尽量避免重复出现相同的代码块。可以通过封装成函数或方法来复用代码,或者使用循环结构进行迭代操作。
  4. 避免过度使用指针:Go语言中的指针可以带来更高的灵活性,但过多使用指针可能会增加代码的复杂性。只有在必要的情况下,才使用指针来传递数据。
  5. 使用错误处理机制:Go语言内置了错误处理机制,使用它来处理可能的错误情况,而不是简单地忽略错误或者直接崩溃。
  6. 编写清晰的文档注释:为你的代码编写清晰的文档注释,描述每个函数、方法和类型的用途、参数和返回值。这有助于他人理解和使用你的代码。
  7. 进行单元测试:编写单元测试可以确保你的代码在不同情况下的正确性。使用Go的内置测试框架,编写详尽的测试用例来覆盖代码的各个分支和边界情况。
  8. 保持代码整洁:注意缩进、代码对齐和空格的使用,以保持代码的可读性。使用代码格式化工具,例如"gofmt",来自动格式化你的代码。
  9. 使用好的命名方式:使用有意义且一致的命名方式来命名变量、函数、方法和类型。这样可以让代码更易于理解和维护。
  10. 及时优化代码:定期回顾和优化你的代码。识别并消除潜在的性能问题,如过多的内存分配、低效的算法等。

1.3 公共符号始终注释

公共符号(如函数、方法、结构体、接口等)的注释是非常重要的,它们有助于其他人理解你的代码并正确使用这些符号。以下是对公共符号进行注释的一些准则:

  1. 使用标准注释格式:Go语言中的公共符号通常使用注释来描述其功能、参数、返回值以及任何重要的注意事项。注释应该以符号的名称开始,并使用完整的句子描述其用途。
  2. 描述符号的功能和目的:注释应该清楚地描述符号的功能和目的。说明它是用于什么目的,以及在何种情况下应该使用它。这对于其他开发人员理解代码非常重要。
  3. 注释参数和返回值:如果符号是一个函数或方法,应该注释参数的含义、类型、以及每个参数的作用。同样地,如果有返回值,应该注释它们的含义和类型。
  4. 注释边界条件和错误处理:注释应该涵盖边界条件和可能发生的错误。描述函数的行为是如何处理异常、错误或无效的输入的。
  5. 提供示例代码:为了更好地理解符号的使用方式,可以提供一些示例代码。这些示例代码应该展示符号的正确用法和预期结果。
  6. 更新注释的变更历史:如果符号的行为或用法发生了变化,应该更新相应的注释,以便其他人了解这些变化。

确保注释与代码保持同步,并且在进行修改时及时更新注释。良好的注释可以提高代码的可读性,减少误解和错误的发生,帮助团队成员更好地合作和开发项目。

1.4 格式化代码

gofmt是Go编程语言中的官方代码格式化工具。它可以帮助你自动格式化Go代码,使其符合Go语言官方的代码风格规范。

1.5 注释

  • 注释应该解释代码作用
  • 注释应该解释代码如何做的
  • 注释应该解释代码实现的原因
  • 注释应该解释代码什么情况会出错

1.6 命名

  • 变量
  • 简洁胜于冗长
  • 缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
  • 例如使用 ServeHTTP 而不是 ServeHttp
  • 使用 XMLHTTPRequest 或者 xmIHTTPRequest
  • 变量距离其被使用的地方越远,则需要携带越多的上下文信息
  • 全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义
  • 函数
  • 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的
  • 函数名尽量简短
  • 当名为 foo 的包某个函数返回类型 Foo 时,可以省略类型信息而不导致歧义
  • 当名为 foo 的包某个函数返回类型 T 时 (T 并不是 Foo) ,可以在函数名中加入类型信息
  • 包名
  • 只由小写字母组成。不包含大写字母和下划线等字符

  • 简短并包含一定的上下文信息。例如 schema、task 等

  • 不要与标准库同名。例如不要使用 sync 或者 strings

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

  • 不使用常用变量名作为包名。例如使用 bufio 而不是 buf

  • 使用单数而不是复数。例如使用 encoding 而不是 encodings

  • 谨慎地使用缩写。例如使用 fmt 在不破坏上下文的情况下比 format 更加简短

1.7 控制流程

  • 如果两个分支中都包含return语句,则可以去除冗余的else
  • 尽量保持正常代码路径为最小缩进
  • 故障问题大多出现在复杂的条件语句和循环语句中

1.8 错误处理

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

1.9 panic的处理

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

1.20 recover的处理

  • recover 只能在被 defer 的函数中使用
  • 嵌套无法生效
  • 只在当前 goroutine 生效
  • defer 的语句是后进先出
  • 如果需要更多的上下文信息,可以 recover 后在 log 中记录当前的调用栈

2.性能优化-Benchmark

在Go语言中,你可以使用内置的testing包来进行基准测试。testing包提供了Benchmark函数,用于定义和执行基准测试。以下是一个简单的示例:

 package main
 ​
 import (
     "fmt"
     "testing"
 )
 ​
 func fibonacci(n int) int {
     if n <= 1 {
         return n
     }
     return fibonacci(n-1) + fibonacci(n-2)
 }
 ​
 func BenchmarkFibonacci(b *testing.B) {
     // 进行性能测试的代码
     for i := 0; i < b.N; i++ {
         fibonacci(20)
     }
 }
 ​
 func main() {
     // 执行基准测试
     result := testing.Benchmark(BenchmarkFibonacci)
 ​
     // 输出基准测试结果
     fmt.Println(result)
 }

在上面的示例中,我们定义了一个BenchmarkFibonacci函数,该函数执行了fibonacci(20)的计算。我们使用testing.B参数来迭代测试代码,并测量代码的性能。在main函数中,我们使用testing.Benchmark函数来执行基准测试,并打印出测试结果。

要运行基准测试,你可以在终端中运行以下命令:

 go test -bench=.

这将执行基准测试,并输出性能测试的结果。

需要注意的是,基准测试的结果会受到多种因素的影响,例如硬件配置、操作系统等。因此,在进行基准测试时,最好在相似的环境中运行测试,并进行多次测试以获取更准确的结果。

2.1 需要初始化的数据结构

slice

map

  • 可以共用空间,返回一个数据结构的一部分

2.2 空结构体节省空间

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

2.3 多线程编程

 type atomicCounter struct {
 i int32
 func AtomicAddOne(c
 *atomicCounter) {
 atomic.AddInt32(&c.i, 1)
 type mutexCounter struct {
 i int32
 m sync.Mutex
 func MutexAddOne(c *mutexCounter) {
 c.m.Lock( )
 c.i++
 c.m.Unlock( )

3.性能分析工具pprof

3.1 原则

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

3.2 使用

pprof是Go语言的性能分析工具之一,可以用于分析程序在运行过程中的性能瓶颈。它可以帮助你找出函数的CPU使用情况、内存分配情况和阻塞情况等。以下是使用pprof的简单示例:

  1. 导入net/http/pprof包:
 import _ "net/http/pprof"

这行代码将导入pprof包,并自动注册了/debug/pprof/路由。

  1. 启动一个HTTP服务来提供性能分析接口:
 import (
     "log"
     "net/http"
 )
 ​
 func main() {
     // 启动HTTP服务
     go func() {
         log.Println(http.ListenAndServe("localhost:6060", nil))
     }()
 ​
     // Your code...
 ​
     // 程序运行中需要等待一段时间以搜集性能数据
 ​
     // 程序退出后,可以通过 http://localhost:6060/debug/pprof/ 访问性能分析界面
 }

在上面的示例中,我们启动了一个在localhost:6060上监听的HTTP服务,以便于访问性能分析结果。

  1. 运行程序,并访问性能分析接口:

运行程序后,访问 http://localhost:6060/debug/pprof/ ,你将看到一些链接,这些链接提供了不同的性能分析报告。例如:

  • /debug/pprof/profile:获取CPU profile的报告。
  • /debug/pprof/heap:获取内存分配情况的报告。
  • /debug/pprof/block:获取阻塞情况的报告。

可以点击其中的链接,或者直接使用go tool pprof命令来分析和可视化性能数据,例如:

 go tool pprof http://localhost:6060/debug/pprof/profile

这将启动一个交互式的命令行工具,展示CPU profile的报告。

  • 输入命令
 go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"
 ​
  • top命令查看进程消耗资源情况
 go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"
 ​

image.png

注意:

  • Flat == Cum,函数中没有调用其他函数
  • Flat == 0,函数中只有其他函数的调用
  • list Eat命令定位到最消耗资源的代码
 list Eat

image.png

  • Heap堆内存

image.png

image.png

  • goroutine泄露会导致内存泄露

3.3 pprof采样原理

  • 采样对象: 函数调用和它们占用的时间
  • 采样率: 100次/秒,固定值
  • 采样时间: 从手动启动到手动结束

4.性能调优

4.1 步骤

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

5.性能优化存在的问题

问题:

  1. 瓶颈定位:确定应用程序的性能瓶颈所在可能是一个挑战。Go的运行时系统自动处理垃圾回收和并发等问题,但也可能导致一些隐藏的性能瓶颈。需要使用适当的工具和技术,如性能分析工具(例如pprof)和基准测试等来定位瓶颈。
  2. 并发安全性:Go鼓励并发编程,但并发编程也带来了一些潜在的问题,如竞争条件和死锁。要确保应用程序在并发环境下的安全性和正确性,并使用适当的并发控制机制来避免性能问题。
  3. 内存管理:Go的垃圾回收机制使得内存管理变得容易,但不正确的内存使用和过度分配仍然可能导致性能问题。需要注意避免内存泄漏、减少内存分配次数,并合理使用内存池等技术来提高性能。
  4. I/O性能:应用程序的I/O操作通常是性能瓶颈之一。需要考虑使用适当的I/O模型和优化技术,如合理使用缓冲区、使用异步I/O、减少系统调用次数等,以提高I/O性能。
  5. 网络通信:在网络应用程序中,网络通信的性能也是一个重要关注点。优化网络通信的技术包括使用连接池、调整网络超时、优化数据传输格式等。
  6. 数据结构和算法选择:合适的数据结构和算法选择可以显著影响应用程序的性能。需要评估选择的数据结构和算法是否高效,并在需要时进行适当的优化。
  7. 编译器优化:Go的编译器在进行一些常见的优化方面已经有很好的表现,但仍可以通过合理的代码设计和优化技巧来进一步提升性能。
  8. 缓存利用:合理利用缓存可以提高数据访问的性能。需要注意避免缓存失效、减少缓存竞争,并使用合适的缓存策略来提高性能。