这是我参与「第五届青训营」伴学笔记创作活动的第 3 天
前言
本章主要介绍了高质量编程和代码的性能调优,通过本章,你将理解如何写出更清晰简洁的代码。在工作中,好的代码可以让人更容易理解代码的开发流程,便于在此基础上进行开发,出现问题的概率也比较低,让团队之间的开发效率更高效。在实际工作过程中,如何对代码的性能问题进行优化,也会让代码的编译过程更加的有效率,节省不一必要的资源浪费。那么,接下来就让我们一起去学习吧!
高质量编程
简介
什么是高质量?
即编写的代码能够达到正确可靠、简洁清晰的目标可称为高质量代码。
高质量标准:
- 正确性:
是否考虑各种边界条件,错误的调用是否能够处理。 - 可靠性:
异常情况或者错误的处理策略是否明确,依赖的服务出现异常是否能够处理。 - 简洁:
逻辑是否简单,后续调整功能或新增功能是否能够快速支持。 - 清晰:
其他人在阅读理解代码的时候是否能清楚明白,重构或者修改功能是否不会担心出现无法预料的问题。
编程原则
高质量代码可以应用的领域众多,不局限于环境的限制。即使各个编程语言的特性和语法各不相同,但高质量代码编译规范遵循的原则上是可以互通的。作为程序员在编码上也应遵照这一规范,方便后期开发。
-
简单性
消除“多余的复杂性”,以简单清晰的逻辑编写代码。不理解的代码无法修复改进。
-
可读性
代码是写给人看的,而不是机器。编写可维护代码的第一步是确保代码可读。
-
生产力
团队整体工作效率非常重要。
编码规范
如何编写高质量的Go代码?
-
代码格式
推荐使用gofmt自动格式化代码gofmt:Go语言官方提供的工具,能自动化格式化Go语言代码为官方统一风格,Goland内置其功能,直接开启即可在保存文件时自动格式化。goimports:也是Go语言官方提供的工具,实际等于gofmt加上依赖包管理,自动增删依赖的包引用、将依赖包按字母排序并分类。
-
注释 --->
Good code has lots of comments,bad code requires lost of comments 好的代码有很多注释,坏的代码需要很多注释。- 注释应该解释代码作用
- 注释应该解释代码如何做的
- 注释应该解释代码实现的原因
- 注释应该解释代码什么情况会出错
- 公共符号始终要注释
-
命名规范
-
简介胜于冗长。
-
缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写。
- 使用
ServeHTTP不是ServeHttp - 使用
XMLHTTPRequest或者xmlHTTPRequest
- 使用
-
变量距离其被使用的地方越远,则需要携带越多的上下文信息
- 全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义。
-
函数名不携带包名上下文信息,尽量简短。
-
package只由小写字母组成,不包含大写字母和下划线等字符;尽量简短并包含一些上下文信息;不与标准库同名。
-
-
控制流程
- 避免嵌套,保持正常流程清晰(如两个分支都包含
return语句,则可以去除冗余的else)。 - 尽量保持正常代码路径为最小缩进,减少嵌套
- 避免嵌套,保持正常流程清晰(如两个分支都包含
-
错误和异常处理
-
简单的错误指的是仅出现一次的错误,且在其他地方不需要捕获该错误。
-
优先使用
erros.New来创建匿名变量来直接表示简单错误。 -
如果有格式化的需求,使用
fmt.Errof。
请看以下例子,
-
func defaultCheckRedirect(req *Request, via []*Request)error{ if len(via) >= 10 { // 使用errors.New创建匿名函数来表示错误 return errors.New("stopped after 10 redirects.") } return nil // 返回空值 } -
错误的
Wrap和Unwrap- 错误的
Wrap实际上是提供了一个error嵌套另一个error的能力,从而生成一个error的跟踪链。 - 在
fmt.Errorf中使用%w关键字来将一个错误关联至错误链中。 - 看以下例子,
-
list, _, err := c.GetBytes(cache.Subkey(a.actionID,"srcfiles")) if err != nil { return fmt.Errorf("reading srcfiles list: %w", err) }
- 错误的
-
错误判定
- 判定一个错误是否为特定错误,使用
erros.ls。 - 不同意使用
==,使用该方法可以判定错误链上的所有错误是否含有特定的错误。 -
data, err = lockedfile.Read(targ) if errors.Is(err, fs.ErrNotExist) { return []byte{}, nil } return data, err - 在错误链上获取特定种类的错误,使用erros.As。
-
if _, err := os.Open("non-existing"); err != nil { var pathError *fs.PathError if errors.As(err, &pathError) { fmt.Println("Failed at path:", pathError.Path) } else { fmt.Println(err) } }
- 判定一个错误是否为特定错误,使用
-
panic- 不建议在业务代码中使用
panic,它出现表示程序无法正常工作了 - 若问题可以解决或屏蔽,建议使用
error代替panic - 当程序启动阶段发生不可逆转的错误时,可以在
init或main函数中使用panic
- 不建议在业务代码中使用
-
recover
生效条件:
- recover只能在defer(注意:defer的语句是后进先出的)函数中使用
- 嵌套无法生效
- 只在当前goroutine生效
- 如果需要更多的上下文信息,可以recover后在log中记录当前调用栈
-
性能优化建议
简介
- 性能优化的前提是满足正确可靠、简洁清晰的质量因素。
- 性能优化是综合评估,有时候时间效率和空间效率可能对立。
1.Benchmark基准测试
- 性能表现需要实际数据衡量,Go语言提供了支持基准性能测试的benchmark工具,使用以下命令
go test -bench=. -benchmen测试
// from fib.go
func Fib(n int) int {
if n < 2 {
return n
}
return Fib(n - 1) + Fib(n - 2)
}
// from fib_test.go
func BenchmarkFib10(b *testing.B) {
// run the Fib funciton b.N times
for n := 0; n < b.N; n++ {
Fib(10
}
}
运行结果如下:
2.slice预分配内存
- 尽可能在使用make()初始化切片时提供容量信息。看下图,可以很清晰地观察到右边代码设置了容量大小,比左边代码的执行时间快了近3倍。
那么为什么有这么大的性能差异?原因就是
- 切片本质上是一个数组片段的描述
- 包括数组指针
- 片段长度
- 片段的容量(不改变内存分配情况下的最大长度)
- 切片操作并不复制切片指向的元素
- 创建一个新的切片会复制原来切片的底层数组
看以下图,当append之后的长度小于等于cap,将会直接利用原底层数组剩余的空间。当append大于cap,则会分配一块更大的区域来容纳新的底层数组。因此,为避免内存发生拷贝,如果知道最终切片的大小,预先设置cap的值能够避免额外的内存分配,获得更好的性能。
slice的另一陷阱:大内存未释放
- 在已有基础上切片,不会创建新的底层数组
- 原切片较大,代码在原切片基础上新建小切片。
- 原底层数组在内存中引用,得不到释放。
- 用copy替代re-slice,通过copy指向新的底层数组,origin不再引用,内存会被回收。
运行结果:
3.map预分配内存
- 不断向map中添加元素的操作会触发map的扩容
- 提前分配好空间可以减少内存拷贝和Rehash的消耗
- 根据实际需要提前预估好需要的空间
4.使用stirng.Builder
- 通过以上图片显示的三种字符串拼接方式比对,使用+拼接性能最差,string.Builder,bytes.Buffer相近,strings.Buffer更快一点。原因如下
- 字符串在Go语言中是不可逆类型,占用内存大小是固定的。
- 使用+每次都会重新分配内存。
- strings.BUidler,bytes.Buffer底层都是[]byte数组 。
- 内存扩容策略,不需要每次拼接重新分配内存。 那么为什么建议使用的是strings.Builder,其实string.Builder,bytes.Buffer相差不大。看下面例子,注意strings.Builder只有一次内存分配,而bytes.Buffer有两次
5.空结构体
- 使用空结构体节省内存
- 空结构体struct{}实例不占据任何的内存空间
- 可作为各种场景下的占位符使用
- 节省资源
- 空结构体本身具备很强的语义,即不需要任何值,仅作为占位符
可以通过以下例子,自行比较
使用atomic包
go中在并发情况下必用到的包,可以基于原子性对数值进行操作,所以经常用来加减锁操作。
- 锁的实现通过操作系统来实现,属于系统调用
- atomic操作是通过硬件实现,效率较高
性能调优
简介
性能调优原则
- 要依靠数据不是猜测
- 要定位最大瓶颈而不是细枝末节
- 不要过早优化和过度优化
性能分析工具pprof
-
说明
- 希望知道应用在声明什么地方耗费了多少CPU、Memory
- pprof是用于可视化和分析性能分析数据的工具
-
pprof功能简介
- pprof排查实战
- 搭建pprof时间项目
- 可通过git来下载
https://github.com/wolfogre/go-pprof-practice - 运行
http://localhost:6060/debug/pprof打开页面,查看数据
采样数据说明:
- allocs:内存分配情况
- blocks:阻塞操作情况
- cmdline:程序启动命令及
- goroutine:当前所有goroutine的堆栈信息
- heap:堆上内存使用情况(同alloc)
- mutex:锁竞争操作情况
- profile: CPU占用情况
- threadcreate:当前所有创建的系统线程的堆栈信息
- trace:程序运行跟踪信息
cmd终端中输入命令go tool pprof + <采样链接>来启动采样,
例如:go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"
输入命令topN,查看占用资源最多的函数
函数意义如下
输入命令list Eat,根据指定的正则表达式查找代码行
输入命令web,生成一张调试图,默认使用浏览器打开。
能很明显看到一个红色大方框,输出q退出终端,把问题代码*Tiger.Eat函数注释掉后,打开活动监视器,可以发现CPU的进程已经降下来。
pprof采样过程和原理
CPU:
Heap:
Goroutine-协程 & ThreadCreate-线程创建:
Block-阻塞 & Mutex-锁:
引用
- 掘金字节内部课-Go高质量编程及性能调优