这是我参与「第三届青训营 -后端场」笔记创作活动的的第3篇笔记。
高质量编程
简介
- 编写的代码能够达到正确可靠、简洁清晰、无性能隐患的目标就能称之为高质量代码(边界条件完善、异常处理,稳定性保证、易读易维护)
- 实际应用场景千变万化,各种语言的特性和语法各不相同,但是高质量编程遵循的原则是相通的
- 高质量的编程需要注意以下原则:简单性、可读性、生产力(团队整体工作效率)
常见编码规范
代码格式
使用 gofmt 自动格式化代码,保证所有的 Go 代码与官方推荐格式保持一致
使用goimports,管理格式、管理依赖包
总结
提升可读性,风格一致的代码更容易维护、需要更少的学习成本、团队合作成本,同时可以降低 Review 成本
编码规范-注释
1️⃣注释应该解释代码作用
2️⃣注释应该解释代码如何做的
3️⃣注释应该解释代码实现的原因
- 适合解释代码的外部原因
- 提供额外上下文
4️⃣注释应该解释代码什么情况会出错
5️⃣公共符号始终要注释
- 包中声明的每个公共的符号:变量、常量、函数以及结构都需要添加注释
- 任何极不明显也不简短的公共功能必须予以注释
- 无论长度或复杂程度如何,对库中的任何函数都必须进行注释
- 例外:不需要注释实现接口的方法
总结
- 代码是最好的注释
- 注释应该提供代码未表达出的上下文信息
编码规范-命名规范
variable
-
简洁胜于冗长
-
缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
- 例如使用ServeHTTP而不是ServeHttp
- 使用XMLHTTPRequest或者xmlHTTPRequest
-
变量距离其被使用的地方越远,则需要携带越多的上下文信息
- 全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义
举例子🌰:
i和index的作用域范围仅限于for循环内部时,index的额外冗长没有增加对于程序的理解
- 将 deadline 替换成 t 降低了变量名的信息量
- t 常指任意时间
- deadline 指截止时间,有特定含义
function
- 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的
- 函数名尽量简短
- 当名为 foo 的包某个函数返回类型 Foo 时,可以省略类型信息而不导致歧义
- 当名为 foo 的包某个函数返回类型 T 时(T 并不是 Foo),可以在函数名中加入类型信息
package
- 只由小写字母组成。不包含大写字母和下划线等字符
- 简短并包含一定的上下文信息。例如 schema、task 等
- 不要与标准库同名。例如不要使用 sync 或者 strings
以下规则尽量满足
- 不使用常用变量名作包名。例如使用buffo而不是buff
- 使用单数而不是复数。例如使用encoding而不是encodings
- 谨慎地使用缩写。例如使用fmt在不破坏上下文的情况下比format更加简短
总结
- 核心目标是降低阅读理解代码的成本
- 关于命名的大多数规范核心在于考虑上下文
- 人们在阅读理解代码的时候也可以看成是计算机运行程序,好的命名能让人把关注点留在主流程上,清晰地理解程序的功能,避免频繁切换到分支细节,增加理解成本
编码规范-控制流程
避免嵌套,保持正常流程清晰
- 如果两个分支中都包含 return 语句,则可以去除冗余的 else
尽量保持正常代码路径为最小缩进,优先处理错误情况/特殊情况,并尽早返回或继续循环来减少嵌套,增加可读性
-
优先处理错误情况/特殊情况,尽早返回或继续循环来减少嵌套
- 优化前:
最常见的正常流程的路径被嵌套在两个if 条件内成功的退出条件是return nil,必须仔细匹配大括号来发现 函数最后一行返回一个错误,需要追溯到匹配的左括号,才能了解何时会触发错误如果后续正常流程需要增加一步操作,调用新的函数,则又会增加一层嵌套
- 优化后
总结
- 线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支
- 正常流程代码沿着屏幕向下移动
- 提升代码可维护性和可读性
- 故障问题大多出现在复杂的条件语句和循环语句中
错误和异常处理
简单错误处理
-
简单的错误指的是仅出观一次的错误,且在其他地方不需要捕获该错误
-
优先使用errors.New来创建匿名变量来直接表示简单错误
-
如果有格式化的需求,使用fmt.Errorf
错误的 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 作为标准。
错误判定
-
判定一个错误是否为特定错误,使用errors.ls
-
不同于使用==,使用该方法可以判定错误链上的所有错误是否含有特定的错误
-
-
在错误链上获取特定种类的错误,使用errors.As
panic
- 不建议在业务代码中使用panic
- 调用函数不包含recover会造成程序崩溃
- 若问题可以被屏蔽或解决,建议使用 error代替panic
- 当程序启动阶段发生不可逆转的错误时,可以在init或 main函数中使用panic
recover
- recover 只能在被defer 的图数中使用
- 嵌套无法生效
- 只在当前goroutine生效
- defer的语句是后进先出
- 如果需要更多的上下文信息,可以 recover 后再 log 中记录当前的调用栈
总结
- panic 用于真正异常的情况
- error 尽可能提供简明的上下文信息,方便定位问题
- recover 生效范围,在当前 goroutine 的被 defer 的函数中生效
性能优化建议
简介
- 性能优化的前提是满足正确可靠、简洁清晰等质量因素
- 性能优化是综合评估,有时候时间效率和空间效率可能对立
- 针对Go语言特性,介绍Go相关的性能优化建议
性能优化建议-Benchmark
- 性能表现需要实际数据衡量
- Go语言提供了支持基准性能测试的benchmark工具
如何使用
结果说明
性能优化建议-Slice
slice 预分配内存
-
在尽可能的情况下,在使用 make() 初始化切片时提供容量信息,特别是在追加切片时
-
性能产生差异的原理
-
切片本质是一个数组片段的描述
- 包括数组指针
- 片段的长度
- 片段的容量(不改变内存分配情况下的最大长度)
-
切片操作并不复制切片指向的元素
-
创建一个新的切片会复用原来切片的底层数组
-
切片有三个属性,指针(ptr)、长度(len) 和容量(cap)。append 时有两种场景:
- 当 append 之后的长度小于等于 cap,将会直接利用原底层数组剩余的空间
- 当 append 后的长度大于 cap 时,则会分配一块更大的区域来容纳新的底层数组
-
-
因此,为了避免内存发生拷贝,如果能够知道最终的切片的大小,预先设置 cap 的值能够获得最好的性能
另一个陷阱:大内存未释放
-
在已有切片基础上创建切片,不会创建新的底层数组
-
场景
- 原切片较大,代码在原切片基础上新建小切片
- 原底层数组在内存中有引用,得不到释放
-
可使用copy替代re-slice
-
-
性能优化建议-Map
map 预分配内存
-
原理
- 不断向 map 中添加元素的操作会触发 map 的扩容
- 根据实际需求提前预估好需要的空间
- 提前分配好空间可以减少内存拷贝和 Rehash 的消耗
性能优化建议-字符串处理
使用 strings.Builder
-
常见的字符串拼接方式
- +
- strings.Builder
- bytes.Buffer
- +
-
strings.Builder 最快,bytes.Buffer 较快,+ 最慢
-
分析
- 字符串在Go 译言中是不可变类型,占用内存大小是固定的
- 使用+每次都会重新分配内存
- strings.Builder, bytes.Buffer底,层都是byte数组
- 内存扩容策略,不需要每次拼接重新分配内存
-
为什么strings.Builder比bytes.Buffer快
-
bytes.buffer 转化为字符串时重新申请了一块空间
- strings.Builder 直接将底层的[]byte 转换成了字符串类型返回
进行了预分配后
性能优化建议-空结构体
空结构体
-
空结构体struct实例不占据任何的内存空间
-
可作为各种场景下的占位符使用
- 节省资源
- 空结构体本身具备很强的语义,即这里不需要任何值,仅作为占位符
空结构体比bool类型更省内存
- 实现Set,可以考虑用map来代替
- 对于这个场景,只需要用到map的键,而不需要值
- 即使是将map的值设置为bool类型,也会多占据1个字节空间
性能优化建议-atomic包
atomic包VS锁
-
原理
- 锁的实现是通过操作系统来实观,属于系统调用
- atomic操作是通过硬件实现,效率比锁高
- sync.Mutex应该用来保护一段逻辑,不仅仅用于保护一个变量
- 对于非数值操作,可以使用atomic.Value,能承载一个interfacet
总结
- 避免常见的性能陷阱可以保证大部分程序的性能
- 普通应用代码,不要一味地追求程序的性能
- 越高级的性能优化手段越容易出现问题
- 在满足正确可靠、简洁清晰的质量要求的前提下提高程序性能
性能调优实战
性能调优简介
-
性能调优原则
- 要依靠数据不是猜测
- 要定位最大瓶颈而不是细枝末节
- 不要过早优化
- 不要过度优化
性能分析工具 pprof
说明
- 希望知道应用在什么地方耗费了多少CPU、Memory
- pprof是用于可视化和分析性能分析数据的工具
性能分析工具 pprof-功能简介
性能分析工具 pprof-排查实战
搭建 pprof 实践项目
GitHub(来自Wolfogre )github.com/wolfogre/go… 项目提前埋入了一些炸弹代码,产生可观测的性能问题
前置准备
下载项目代码,能够编译运行 会占用1CPU核心和超过1GB的内存 浏览器查看指标 浏览器输入 http://localhost:6060/debug/pprof/
CPU
我们先从CPU问题排查开始,不同的操作系统工具可能不同,我们首先使用自己熟悉的工具看看程序进程的资源占用,CPU占用了58%,显然这里是有问题的
go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"
这里我们使用go tool pprof +采样链接来启动采样。 连接中就是刚刚「|炸弹」程序暴露出来的接回,连接结尾的tporief表采样的对金是CU使用。如果你在流说克器里直接打开这个钳接,会定动一个60s的采祥,并在纯束后下数文件。这里我和们加上scndses = 10的参势数,让他采样10s.
稍等片刻,我们需要的采样数据已经记录和下载完成,并展示出pprof终端
命令:top 查看占用资源最多的函数
命令:list
根据指定的正则表达式查找代码行
输入:list Eat
命令:web 调用关系可视化
以上命令可以定位[炸弹]的位置,用q退出终端。
接下来就是将[炸弹]删除:将/animal/felidae/tiget/tiget.go文件下的 23-26行注释,重新运行
heap-堆内存
cpu降下来了,但内存还是很高
终端输入:
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
这里浏览器打开的是[内存占用]
点击view按钮切换到top,点击路径
点击view按钮切换到source
根据top图路径打开mouse.go,在steal()这个函数会向固定的Buffer中不断追加1MG内存,直到Buffer达到1GB大小位置,和我们再Graph视图中发现的情况一致。我们将代码注释掉。
至此,[炸弹]已经拆除两个了
重新运行程序,内存降低了
我们注意到右上角有个 unknown inuse_space。
我们打开sample菜单,会发现最内存实际上提供了四种指标。 在堆内存采样中,默认展示的是inuse_space视图,只展示当前持有的内存,但如果有的内存已经释放,这时inuse采样就不会展示了。我们切换到alloc_ space指标。后续分析下alloc的内存问题
点击 alloc_space,鼠标点击一下dog
在进去 source,然后根据路径找到dog.go,将128M这行代码注释掉
至此,内存部分的[炸弹]已经被全部拆除。
goroutine-协程
goroutine 泄露也会导致内存泄露 Golang是一门自带垃圾回收的语言,一般情况下内存泄露是没那么容易发生的。 但是有一种例外: goroutine是很容易泄露的,进而会导致内存泄露。
打开http;//ocalhost6060/debug/pprof/,发现「炸弹」程序已经有65条goroutine在运行了,这个量级并不是很大,但对于一个简单的小程序来说还是很不正常的。
终端输入:
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine" 1 就会出现一张非常长的调用关系图,但是这种比较不好阅读,推荐使用火焰图
火焰图:
由上到下表示调用顺序 每一块代表一个函数,越长代表占用CPU的时问更长 火焰图是动态的,支持点击块进行分析 打开view菜单,切换到flame graph视图,可以看到刚刚的节点被堆叠起来。
图中自顶向下展示了各个调用,表示各个函数之间的层级关系,每一行中,条形越长代表消耗的资源占比越多,可以看到wolfy资源占比95.24%,所以我们打开source。
支持搜索,在Source视图下搜索 wolf
发现每次都会发起十次无意义的goroutine,每次等待30秒菜退出,导致goroutine泄露。这里为了模拟泄漏场景,只设置了30秒,如果发起的goroutine没有退出,不断有新的goroutine被启动,内存占比持续增长,cpu调度压力不断增大,最终进程会被系统kill掉。
我们注释掉代码,重启,可以看到goroutine恢复正常水平
mutex-锁
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex" 1 修改链接后缀,改成mutex,然后打开网页观察,发现存在1个锁操作同样地,在Graph视图中定位到出问题的函数在Wolf.Howl() 然后在Source视图*中定位到具体哪—行发生了锁竞争 在这个函数中,goroutine足足等待了1秒才解锁,在这里阻塞住了,显然不是什么业务需求,注释掉。
block-阻塞
重启后,可以看到页面中block还剩两个
连接地址结尾换成block
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block" 1
和刚才一样,graph到source视图切换,可以看到,在*Cat.Pee()函数中读取了一个time.Afterl)生成的channel这就导致了这个goroutine实际上阻塞了1秒钟,而不是等待了1秒钟。我们注释掉,不用重启
两个blocak为什么只展示了一个 打开block页面,可以看到第二个阻塞操作发生在http handle中,这个操作是符合预期的,不用管。
剩下的以后再写!