高质量编程与性能调优实战
1.如何编写更简洁清晰的代码
1.1 高质量代码
编写的代码能够达到正确可靠、简洁清晰的目标可称之为高质量代码
- 各种边界条件是否考虑完备
- 异常情况处理,稳定性保证
- 易读易维护
1.2 开发原则
- 遵循Go的命名规范:按照Go语言的命名规范,使用有意义的变量、函数和方法名称,避免使用缩写或单个字母作为名称。
- 使用短小的函数和方法:拆分较大的函数和方法为小型的、单一的功能单元。这样做有助于提高代码的可读性,并使得代码更易于测试和维护。
- 减少重复代码:尽量避免重复出现相同的代码块。可以通过封装成函数或方法来复用代码,或者使用循环结构进行迭代操作。
- 避免过度使用指针:Go语言中的指针可以带来更高的灵活性,但过多使用指针可能会增加代码的复杂性。只有在必要的情况下,才使用指针来传递数据。
- 使用错误处理机制:Go语言内置了错误处理机制,使用它来处理可能的错误情况,而不是简单地忽略错误或者直接崩溃。
- 编写清晰的文档注释:为你的代码编写清晰的文档注释,描述每个函数、方法和类型的用途、参数和返回值。这有助于他人理解和使用你的代码。
- 进行单元测试:编写单元测试可以确保你的代码在不同情况下的正确性。使用Go的内置测试框架,编写详尽的测试用例来覆盖代码的各个分支和边界情况。
- 保持代码整洁:注意缩进、代码对齐和空格的使用,以保持代码的可读性。使用代码格式化工具,例如"gofmt",来自动格式化你的代码。
- 使用好的命名方式:使用有意义且一致的命名方式来命名变量、函数、方法和类型。这样可以让代码更易于理解和维护。
- 及时优化代码:定期回顾和优化你的代码。识别并消除潜在的性能问题,如过多的内存分配、低效的算法等。
1.3 公共符号始终注释
公共符号(如函数、方法、结构体、接口等)的注释是非常重要的,它们有助于其他人理解你的代码并正确使用这些符号。以下是对公共符号进行注释的一些准则:
- 使用标准注释格式:Go语言中的公共符号通常使用注释来描述其功能、参数、返回值以及任何重要的注意事项。注释应该以符号的名称开始,并使用完整的句子描述其用途。
- 描述符号的功能和目的:注释应该清楚地描述符号的功能和目的。说明它是用于什么目的,以及在何种情况下应该使用它。这对于其他开发人员理解代码非常重要。
- 注释参数和返回值:如果符号是一个函数或方法,应该注释参数的含义、类型、以及每个参数的作用。同样地,如果有返回值,应该注释它们的含义和类型。
- 注释边界条件和错误处理:注释应该涵盖边界条件和可能发生的错误。描述函数的行为是如何处理异常、错误或无效的输入的。
- 提供示例代码:为了更好地理解符号的使用方式,可以提供一些示例代码。这些示例代码应该展示符号的正确用法和预期结果。
- 更新注释的变更历史:如果符号的行为或用法发生了变化,应该更新相应的注释,以便其他人了解这些变化。
确保注释与代码保持同步,并且在进行修改时及时更新注释。良好的注释可以提高代码的可读性,减少误解和错误的发生,帮助团队成员更好地合作和开发项目。
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的简单示例:
- 导入
net/http/pprof包:import _ "net/http/pprof"这行代码将导入
pprof包,并自动注册了/debug/pprof/路由。
- 启动一个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服务,以便于访问性能分析结果。
- 运行程序,并访问性能分析接口:
运行程序后,访问 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"
注意:
- Flat == Cum,函数中没有调用其他函数
- Flat == 0,函数中只有其他函数的调用
- list Eat命令定位到最消耗资源的代码
list Eat
- Heap堆内存
- goroutine泄露会导致内存泄露
3.3 pprof采样原理
- 采样对象: 函数调用和它们占用的时间
- 采样率: 100次/秒,固定值
- 采样时间: 从手动启动到手动结束
4.性能调优
4.1 步骤
- 建立服务性能评估手段
- 分析性能数据,定位性能瓶颈
- 重点优化项改造
- 优化效果验证
5.性能优化存在的问题
问题:
- 瓶颈定位:确定应用程序的性能瓶颈所在可能是一个挑战。Go的运行时系统自动处理垃圾回收和并发等问题,但也可能导致一些隐藏的性能瓶颈。需要使用适当的工具和技术,如性能分析工具(例如
pprof)和基准测试等来定位瓶颈。- 并发安全性:Go鼓励并发编程,但并发编程也带来了一些潜在的问题,如竞争条件和死锁。要确保应用程序在并发环境下的安全性和正确性,并使用适当的并发控制机制来避免性能问题。
- 内存管理:Go的垃圾回收机制使得内存管理变得容易,但不正确的内存使用和过度分配仍然可能导致性能问题。需要注意避免内存泄漏、减少内存分配次数,并合理使用内存池等技术来提高性能。
- I/O性能:应用程序的I/O操作通常是性能瓶颈之一。需要考虑使用适当的I/O模型和优化技术,如合理使用缓冲区、使用异步I/O、减少系统调用次数等,以提高I/O性能。
- 网络通信:在网络应用程序中,网络通信的性能也是一个重要关注点。优化网络通信的技术包括使用连接池、调整网络超时、优化数据传输格式等。
- 数据结构和算法选择:合适的数据结构和算法选择可以显著影响应用程序的性能。需要评估选择的数据结构和算法是否高效,并在需要时进行适当的优化。
- 编译器优化:Go的编译器在进行一些常见的优化方面已经有很好的表现,但仍可以通过合理的代码设计和优化技巧来进一步提升性能。
- 缓存利用:合理利用缓存可以提高数据访问的性能。需要注意避免缓存失效、减少缓存竞争,并使用合适的缓存策略来提高性能。