1.1 简介
高质量:编写的代码能否达到正确可靠、简洁清晰的目标
- 各种边界条件是否考虑完备
- 异常情况处理,稳定性保证
- 易读易维护
编程原则
- 简单性
- 消除多余的重复性,以简单清晰的逻辑编写代码
- 不理解的代码无法修复改进
- 可读性
- 代码是写给人看的,并不是机器
- 编写可维护代码的第一步是确保代码可读
- 生产力
- 团队整体工作效率非常重要
1.2 编码规范
如何编写高质量的Go代码
1.2.1 代码格式
推荐使用gofmt
自动格式化代码
主要有两种:
gofmt
goimports
实际上等于gofmt
加上依赖包管理,自动增删依赖包的引用、将依赖包按字母序排序并分类
1.2.2 注释
注释的作用
-
解释代码作用:适合注释公共符号
-
解释代码如何做的:适合注释实现过程
-
解释代码实现的原因:适合解释代码的外部因素,提供额外的上下文
-
解释代码什么情况会出错:适合解释代码的限制条件
-
公共符合始终要注释:
- 包中声明的每个公共的符号、常量、变量、函数以及结构都需要添加注释
- 任何公共功能都必须予以注释
- 库中的任何函数都要进行注释
- 不需要注释实现接口的方法
1.2.3 命名规范
变量:
- 简洁胜于冗长
- 缩略词全大写,但是其位于变量开头且不需要导出时,使用全小写
- 使用ServerHTTP而不是ServerHttp
- 使用XMLHTTPRequest 或者xmlHTTPRequest
- 变量距离被使用的地方越远,需要携带越多的上下文信息
函数:
- 函数名不携带包名的上下文信息
- 尽量简短
- 名为foo的包某个函数返回类型Foo时,可以省略类型信息
- 名为foo的包返回类型T时,可以加入类型信息
package
- 只由小写字母组成。不包含大写字母和下划线等字符
- 简短并包含一定的上下文信息。例如
schema
、task
等 - 不要与标准库同名。例如不要使用
sync
或者strings
- 以下规则尽量满足,以标准库包名为例
- 不使用常用变量名作为包名。例如使用
bufio
而不是buf
- 使用单数而不是复数。例如使用
encoding
而不是encodings
- 谨慎地使用缩写。例如使用
fmt
在不破坏上下文的情况下比format
更加简短
- 不使用常用变量名作为包名。例如使用
小结
-
核心目标是降低阅读理解代码的成本
-
重点考虑上下文信息,设计简洁清晰的名称
1.2.4 控制流程
-
避免嵌套,保证正常流程清晰。比如如果两个分支都有
return
,那么第二个的else
的应当省略 -
尽量保持正常代码路径为最小缩进:优先处理错误或特殊情况,尽早返回或继续循环来减少嵌套
总结
-
线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支
-
正常流程代码沿着屏幕向下移动
-
提高代码的维护性和可读性
-
故障问题大多出现在复杂的条件/循环语句里
1.2.5 错误和异常处理
简单错误
- 简单的错误指的是仅仅出现一次的错误,而且在其他地方不需要捕获该错误
- 优先使用
errors.New
来创建匿名变量直接简单的表示错误,如return errors.New("Please input a number")
- 如果有格式化需求,使用
fmt.Errorf
错误的Wrap和Unwrap
实际上是提供了error
嵌套另一个error
的能力
在fmt.Errorf
中使用%w关键字将一个错误关联到错误链中
错误判断
判断一个错误是否为特定错误,使用errors.Is
不能用==,why?
错误链上会有很多种类的错误
在错误链上获取特定种类的错误,使用errors.As
panic
不建议在业务代码里使用
如果当前 goroutine
中所有 deferred
函数都不包含 recover
就会造成整个程序崩溃
若问题可以被屏蔽或解决,建议使用error
如果程序启动阶段发生不可逆转的错误,可以在init/main
函数里使用
recover
只能在被defer
的函数里使用
嵌套无法生效
只在当前 goroutine
生效
defer
的语句后进先出
如果需要更多的上下文信息,在log
里记录当前的调用栈
总结
error
尽可能提供简明的上下文信息,方便定位问题
panic
用于真正异常的情况
recover
生效范围,在当前 goroutine
的被 defer
的函数中生效
1.3 性能优化建议
性能优化的前提是满足正确可靠、简洁清晰等质量因素
性能优化是综合评估,有时候时间效率和空间效率可能对立
1.3.1 Benchmark
如何使用
性能表现需要实际数据衡量
go test -bench=. -benchmem
1.3.2 Slice
预分配内存
尽可能的在使用make
初始化切片的时候提供容量信息,如data:=make([]int,0,size)
原理:在创建一个新的切片时实际上会复用原来切片的底层数组。比如append
场景,当append
之后的长度小于等于容量的时候,会直接利用原底层数组剩余的空间;否则,就分配一块更大的区域来容纳新的底层数组。
这样会导致的另一个问题就是大内存未释放。
在已有切片基础上创建切片,不会创建新的底层数组
比如在原切片较大时,如果代码在原切片基础上新建小切片,原底层数组在内存里有引用,无法释放。这时候应该用copy
来替代直接引用。
1.3.3. map
- 不断向 map 中添加元素的操作会触发 map 的扩容
- 根据实际需求提前预估好需要的空间
- 提前分配好空间可以减少内存拷贝和 Rehash 的消耗
1.3.4 字符串处理
常见的字符串拼接方法
+
strings.Builder
bytes.Buffer
strings.Builder
>bytes.Buffer
>+
原理
字符串在 Go 语言中是不可变类型,占用内存大小是固定的
使用+的时候每次都会重新分配内存
strings.Builder
和 bytes.Buffer
底层都是 []byte 数组
内存扩容策略,不需要每次拼接重新分配内存
bytes.Buffer
转化为字符串时重新申请了一块空间
strings.Builder
直接将底层的 []byte 转换成了字符串类型返回
其他优化
可以通过Grow
来实现内存的预分配,提高效率
1.3.5 使用空结构体节省内存
空结构体struct{}
实例不占据任何内存空间,可以作为任何场景下的占位符使用,有利于节省资源。
比如实现简单的set,就可以用map来替代
1.3.7 使用atomic包
多线程开发的时候,可以使用sync.Mutex
加锁的方式,也可以用atomic.AddInt32
方法。后者的效率更高。
原理
锁的实现是通过操作系统来实现,属于系统调用;而atomic
是通过硬件实现,效率高。
使用场景
sync.Mutex
应该用于保护一段逻辑
非数值操作可以使用atomic.Value
小结
-
避免常见的性能陷阱可以保证大部分程序的性能
-
针对普通应用代码,不要一味地追求程序的性能
-
越高级的性能优化手段越容易出现问题
-
在满足正确可靠、简洁清晰等质量要求的前提下提高程序性能
2.性能优化实战
2.1 简介
性能调优原则
- 要依靠数据不是猜测
- 要定位最大瓶颈而不是细枝末节
- 不要过早优化
- 不要过度优化
2.2 性能分析工具pprof
2.2.1 简介
可以知道应用在什么地方耗费了多少 CPU、memory 等运行指标
pprof 是用于可视化和分析性能分析数据的工具
2.2.2 排查实战
使用说明
可视化工具下载地址Download | Graphviz
运行方式:先输入go build
,再输入go run main.go
CPU
在终端执行命令,其中seconds=10
表示采取最近10s的内容
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=10
然后输入top
,查看 CPU 占用较高的调用
注释掉该部分代码然后继续进行操作
heap堆内存
图形化页面
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
如果没安装graphviz
的话可以继续使用终端的方式查看,下面为了方便使用图形化页面。
打开后的网站提供了以下view形式
点击Graph
视图得到
切换到source
模式查看,发现在mouse.Steal
的50行,会向固定的Buffer
里不断追加1M内存,直到1GB位置。
注释掉这几行代码即可
=================================================================
观察到右上角有一个unknown_inuse_space
,所以打开sample
菜单,会发现堆内存实际上提供了4种指标,默认展示的是inuse_space
视图,只展示当前持有的内存,但是如果有的内存释放,就不再展示了,切换到alloc_space
指标。
发现dog.run
每次都会申请16MB的大小,但是分配结束后马上被GC了,所以在默认指标下看不出来。注释掉代码即可。
goroutine协程
有时候goroutine
泄露也会导致内存泄露,而且goroutine
是很容易泄露的。
在终端输入命令
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"
点击view切换到flamegraph模式
从上到下表示调用顺序,每一块表示一个函数,越长代表占用CPU的时间越长。
火焰图是动态的,支持点击块进行分析。
可以发现wolf.drink
的调用为92.31%
切换到source模式,这个页面支持搜索
函数每次都会发起10条无意义的协程,等待30s后才退出,就导致goroutine的泄露。如果发起的协程没有退出,同时不断有新的协程被启动,对应的内存占用持续增长,CPU调度压力也不断增大,最终进程会被系统kill掉。
注释掉这段代码
mutex锁
现在排查mutex,运行代码后输入
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"
切换到souce模式,发现
在这个函数里,goroutine足足等了1s才解锁,在这里阻塞住了,显然不是业务需求,注释掉
block阻塞
在程序里,除了锁的竞争会导致阻塞之外,还有很多逻辑也会导致阻塞,比如读取一个channel。
查看6060端口页面发现阻塞操作还剩2个。
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"
切换到source模式
不知道为什么跟ppt的不太一样,这里查看的时候一次就查看出了两个block
2.2.3 采用过程和原理
CPU
采样对象:函数调用和他们占用的时间
采样率:100次/秒,固定值
采样时间:从手动启动到手动结束
—共有三个相关角色:进程本身、操作系统和写缓冲。
启动采样时,进程向OS注册一个定时器,OS会每隔10ms向进程发送一个SIGPROF信号,进程接收到信号后就会对当前的调用栈进行记录。与此同时,进程会启动一个写缓冲的goroutine,它会每隔100ms从进程中读取已经记录的堆栈信息,并写入到输出流。
当采样停止时,进程向OS取消定时器,不再接收信号,写缓冲读取不到新的堆栈时,结束输出。
Heap
采样程序通过内存分配器在堆上分配和释放的内存,记录分配/释放的大小和数量
采样率:每分配512KB记录一次
采样时间:从程序运行开始到采样时
计算方式:inuse=alloc-free
Goroutine和ThreadCreate创建
Goroutine
记录所有用户发起且在运行钟的goroutine runtime.main的调用栈信息
ThreadCreate
记录程序创建的所有系统线程的信息
Block
阻塞操作:采样阻塞的次数和耗时;采样率是阻塞耗时超过阈值的才会被记录。
Mutex
采样争抢锁的次数和耗时;采样率只记录固定比例的锁操作
2.3 性能调优案例
- 业务服务优化
- 基础库优化
- Go语言优化
2.3.1 业务服务优化
基础概念
服务:能够单独部署,承载一定功能的程序
依赖:服务A的功能实现依赖服务B的响应结果
调用链路:能够支持一个接口请求的相关服务集合及其相互之间的依赖关系
基础库:公用的工具包、中间件
流程
-
建立服务性能评估手段
- 服务性能评估的方式:单独的benchmark无法满足复杂逻辑分析,而且不同负载情况下的性能表现有差异
- 请求流量构造:不同请求参数覆盖逻辑不同,需要尽可能的模拟线上的真实流量情况
- 压测范围:可以是单机器压测,也可以是集群压测
- 性能数据采集:可以是单机性能数据,也可以是集群性能数据
-
分析性能数据,定位性能瓶颈
- 使用基础组件不规范
- 使用日志不规范
- 高并发场景性能优化不足
-
重点优化项改造
- 性能优化的前提是保证正确性
- 需要对比优化前后的响应数据
-
优化效果验证
- 重复压测验证,使用同样的数据进行压测
- 需要结合线上的表现再进行分析改进:比如关注服务监控、逐步放量、收集性能数据
以上都是针对单个服务的优化过程
在熟悉服务器的整体部署情况后,可以针对具体的接口链路进行分析调优,只能适用于具体的场景,但是更加能够合理的利用资源
2.3.2 基础库优化
适用范围更广的是基础库的优化,大概可以结合下这篇文章几个秒杀 Go 官方库的第三方开源库 - 掘金 (juejin.cn)
2.3.3 Go语言优化
编译器和运行时优化
比如优化内存分配策略和代码编译流程,生成更高效的程序
优点为接入简单,只需要调整编译配置,而且通用性强
2.4 总结
性能调优要依靠数据而不是单纯的猜测
可以使用pprof来排查性能问题,理解基本原理
性能调优首先要保证正确性