1.1 简介
高质量:编写的代码能否达到正确可靠、简洁清晰的目标
- 各种边界条件是否考虑完备
- 异常情况处理,稳定性保证
- 易读易维护
编程原则
- 简单性
- 消除多余的重复性,以简单清晰的逻辑编写代码
- 不理解的代码无法修复改进
- 可读性
- 代码是写给人看的,并不是机器
- 编写可维护代码的第一步是确保代码可读
- 生产力
- 团队整体工作效率非常重要
1.2 编码规范
如何编写高质量的Go代码
1.2.1 代码格式
推荐使用gofmt自动格式化代码
主要有两种:
gofmtgoimports实际上等于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.Builderbytes.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来排查性能问题,理解基本原理
性能调优首先要保证正确性