在写代码的过程中,除了功能效率达到最好,清晰的代码也是非常重要的。
高质量编程
简介
定义
Q:高质量?什么是高质量?
A:编写的代码能够达到正确可靠、简洁清晰的目标可称为高质量代码。(比如:各种边界条件是否考虑完备、异常情况处理与稳定性保证、易读易维护能保证团队其他成员能够读懂代码并进行功能新增优化等)
编程原则
简单性、可读性、生产力,这三个特点是相通的。确保遵循这三个原则可以保证代码后续的修复改进并可以保证团队较高的工作效率。
编码规范
各大公司会发布编程规范,例如对代码格式、注释、命名规范、控制流程、错误和异常处理等。
代码格式
推荐使用gofmt自动格式化代码!
注释
- 包中声明的每个公共的符号:变量、常量、函数以及结构都需要添加注释。
- 任何既不明显也不简短的公共功能必须予以注释。
- 无论长度或复杂程度如何,对库中的任何函数都必须进行注释。
- 不需要注释实现接口的方法!
Good code has lots of comments, bad code requires lots of comments.
注释应该做的:
- 解释代码作用
- 适合注释公共符号
- 解释代码如何做的
- 实际功能的解释
- 解释代码实现的原因
- 除了代码功能以外的外部因素,提供额外的上下文
- 解释代码什么情况会出错
- 也就是代码的限制条件。使用者可以快速了解代码的使用
好的注释范例:
命名规范
核心:降低阅读理解代码的成本!!!
variable
-
简洁胜于冗长
-
缩略词全大写,如:ServeHTTP
当缩略词位于变量开头且不需要导出时全小写:如xmlHTTPRequest或XMLHTTPRequest
它们的作用于范围都仅仅局限于for循环内部,用index并没有增加对程序的理解,所以没必要用index。
deadline指的是截止时间,有特定含义。当外部调用该函数的时候,deadline更加清晰明了。
function
- 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的。
- 函数名尽量简短当名为Foo的包某个函数返回类型Foo 时,可以省略类型信息而不导致歧义。
- 当名为Foo 的包某个函数返回类型 T 时 (T 并不是 Foo),可以在函数名中加入类型信息。
Q:
A:在外部调用的时候,是采取包名.函数名进行调用。对于题目而言,第一种命名方式更好,调用的时候直接http.Server,无需再重复HTTP。
package
- 只由小写字母组成。不包含大写字母和下划线等字符。
- 简短并包含一定的上下文信息。例如schema、task 等。
- 不要与标准库同名。例如不要使用sync或者strings。
以下规则尽量满足,以标准库包名为例:
- 不使用常用变量名作为包名。例如使用bufio而不是buf。
- 使用单数而不是复数。例如使用encoding 而不是encodings。
- 谨慎地使用缩写。例如使用fmt在不破坏上下文的情况下比format更加简短。
控制流程
-
避免
if-else嵌套。如果if和else语句中都包含return语句,那么可以去掉冗余的else【吼吼吼,这个我做到啦!】
-
尽量保持正常代码路径为最小缩进。这要求我们优先处理错误情况,尽早返回或继续循环以减少嵌套。
【我就是经典bad啊啊啊!以后要向下面这种靠齐!】
错误和异常处理
简单错误
指的是仅出现一次的错误,且其他地方无需捕获。优先使用error.New。如果有格式化要求,就用fmt.Errorf。
错误的Wrap和Unwrap
提供一个error嵌套ing一个error的能力,从而生成一个error的跟踪链。
实现:在fmt.Errorf中使用%w来将一个错误关联至错误链中。
错误判定
使用errors.Is。它不同于==,它可以判定错误链上的所有错误是否含有特定错误。
使用errors.As,在错误链上获取特定种类的错误。 可以把特定错误的命令取出来。
panic
- 业务代码中不建议使用。
- 如果问题可以被屏蔽或解决,建议使用error代替panic。
- 当程序启动阶段发生不可逆转的错误时,可以在int或main函数中使用panic。
recover
- 只能在被
defer的函数中使用。defer的语句是后进先出。 - 嵌套无法生效!!!
- 只会在当前goroutine生效。
性能优化指南
优化建议
前提:满足正确可靠、简洁清晰等质量因素。
不能为了优化而优化!
Go语言提供了支持基准性能测试的benchmark工具
slice预分配内存
尽可能在使用make()初始化切片时提供容量信息。
提前分配容量,执行时间会大大简短。
底层扩容图示:
原切片较大、代码在原切片基础上创建小切片,或原底层数组在内存中有引用、得不到释放,那么会造成大内存未释放的问题。
此时,我们需要用copy代替re-slice
map预分配内存
和slice差不多。提前分配好空间可以减少内存拷贝和Rehash的消耗。
使用string.Builder
常见的字符串拼接方式。
这种方法的性能相对于+和bytes.Buffer更好。
字符串在Go中是不可变类型,占用内存的大小是固定的。所以如果使用+,则每次都会重新分配内存。而strings.Builder和bytes.Buffer底层都是[]byte数组,不需要每次都拼接重新分配内存。
类似于java中的StringBuffer
为什么
strings.Builder比bytes.Buffer性能更好呢?
strings.Builder直接将底层的[]byte转化成了字符串类型返回,而bytes.Buffer在转化为字符串时重新申请了一块空间。
优化strings.Builder:使用前文提到的预分配内存。
空结构体
空结构体struct{}不占据任何的内存空间,可以节省资源,仅作为占位符。
atomic包
对于非数值操作,可以使用atomic.Value。
可以提高效率!
性能调优实战
性能优化原则
依靠数据,定位最大瓶颈,不过早、过度优化。
性能分析工具 pprof - 排查实战
CPU
使用命令:go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"
topN
命令:
topN用于查看占用资源最多的函数。
| 命令 | 意义 |
|---|---|
| flat | 当前函数本身的执行耗时 |
| flat% | flat占CPU总时间的比例 |
| sum% | 上面每一行的flat%总和 |
| cum | 指当前函数本身加上其调用函数的总耗时 |
| cum% | cum占CPU总时间的比例 |
- 当函数中没有调用其他函数时,Flat == Cum
- 当函数中只有其他函数的调用时,Flat == 0
list
命令:
list根据指定的正则表达式查找代码行。
web
命令:
web调用关系可视化
我们此时想分析堆内存,故而使用命令:go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
【为啥我打不开这个网页呢???】
如果打开了网页,SAMPLE下对应的alloc_objects是指程序累计申请的对象数,alloc_space是指程序累计申请的内存大小,inuse_objects是指程序当前持有的对象数,inuse_space是指程序当前占用的内存大小。
goroutine 协程
协程泄露也会导致内存泄露
命令:go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine
由上到下表示调用顺序
每一块代表一个函数,越长代表占用CPU时间更长
火焰图是动态的
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次/秒。
- 采样时间:手动启动到手动结束。
开始采样 -> 设定信号处理函数 -> 开启定时器
停止采样 -> 取消信号处理函数 -> 关闭定时器
Heap-堆内存
- 采样程序通过内存分配器在堆上分配和释放的内存,记录分配/释放的大小和数量。
- 采样率:每分配512KB记录一次,可在运行开头修改,1为每次分配均记录。
- 采样时间:从程序运行开始到采样时。
- 采样指标:alloc_.space, alloc.objects, inuse_space, inuse.objects
- 计算方式:inuse = alloc - free
Goroutine协程 & ThreadCreate线程 创建
-
Goroutine
- 记录所有用户发起且在运行中的goroutine
runtime.main的调用栈信息
-
ThreadCreate
- 用于记录程序创建的所有系统线程的信息
Block阻塞 & Mutex锁
阻塞操作采样率是阻塞操作超过阈值的才会被记录,1为每次阻塞均记录。
锁竞争采样率是只记录固定比例的锁操作,1为每次加锁均记录。
性能调优案例
在现实项目中,会涉及到业务服务优化、基础库优化、Go语言优化。
业务服务优化
基本概念
-
白色背景:客户端。
-
橙色背景:网关。
-
蓝色背景:业务服务。
整条完整的链路称为调用链路,能够支持一个接口请求的相关服务集合及其相互之间的依赖关系。
服务需要能够单独部署、承载一定功能。
流程
建立服务性能评估手段,分析性能数据、定位性能瓶颈,重点优化项改造(改造项目中占用最多),优化效果验证。
-
服务性能评估手段
- 服务型评估方式:不同负载情况下性能表现差异。
- 请求流量构造:不同请求参数覆盖的逻辑不同,线上真实流露的情况等。
- 压测范围:单机器压测和集群压测。 会生成压测报告,包括压测曲线等。
- 性能数据采集:单机性能数据和集群性能数据。
-
分析性能数据、定位性能瓶颈
- 使用库不规范问题:分析数据结果,例如在火焰图中,如果某个点占用时间比较长,底层逻辑可能是CPU占用时间较多,定位到代码,就可以看到不规范的地方,然后进行修改。
- 高并发场景优化不足:在高峰期和低峰期分别采样,通过两个火焰图的对比,可以发现某些库在高并发场景会有问题,于是对这方面进行修改。
-
重点优化项改造
- 使用diff将线上请求数据录制回放。
-
优化效果验证
- 重复压测验证,通过压测报告可以得到优化的效果。
进一步优化
通过上面的优化流程后,我们可以考虑分析链路或规范上有服务调用接口来进行进一步的优化。
基础库优化
AB实验SDK优化
分析基础库核心逻辑和性能瓶颈,通过内部压测和推广业务服务落地验证。
Go语言优化
比如编译器、运行时优化。它的接入很简单,只需要调整编译配置,而且通用性也很强。
简单项目的性能优化
下面我尝试一下优化简单的文件输出代码。
原代码:
package main
import (
"bufio"
"fmt"
"log"
"os"
)
func main() {
// 打开文件
file, err := os.Open("example.txt")
if err != nil {
log.Fatalf("无法打开文件: %v", err)
}
// 确保文件在函数返回时关闭
defer file.Close()
// 使用 bufio 包读取文件内容
scanner := bufio.NewScanner(file)
for scanner.Scan() {
// 打印每一行
fmt.Println(scanner.Text())
}
// 检查读取是否出错
if err := scanner.Err(); err != nil {
log.Fatalf("读取文件出错: %v", err)
}
}
从减少内存分配和垃圾回收以及并发处理的方面进行代码优化:
package main
import (
"bufio"
"fmt"
"log"
"os"
"sync"
)
func main() {
// 打开文件
file, err := os.Open("example.txt")
if err != nil {
log.Fatalf("无法打开文件: %v", err)
}
// 确保文件在函数返回时关闭
defer file.Close()
// 使用 bufio 包读取文件内容
scanner := bufio.NewScanner(file)
// 创建一个等待组,用于等待所有 goroutine 完成
var wg sync.WaitGroup
// 定义一个函数,用于读取文件的一部分并打印
readAndPrint := func() {
defer wg.Done()
for scanner.Scan() {
// 打印每一行
fmt.Println(scanner.Text())
}
}
// 启动多个 goroutine 来并发读取文件
numWorkers := 4 // 这边我选择4个goroutine
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go readAndPrint()
}
// 等待所有 goroutine 完成
wg.Wait()
// 检查读取是否出错
if err := scanner.Err(); err != nil {
log.Fatalf("读取文件出错: %v", err)
}
}
结语
因为我的pprof的ui页面一直无法运行,所以有关性能优化的无法实操。
这里想问万能而伟大的网友们,关于graphviz为什么安装了、配置了但仍然用不了这个问题,我目前还没有解决。下面这个就是我打开的网页的提示。
如果可以,大家可不可以评论区提供解决方案,我去捞捞~