这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记
高质量编程
本文主要针对高质量的Go编程。
如何编写高质量的Go代码
人们普遍认可的“高质量”:
- 正确性: 是否考虑各种边界条件,错误的调用是否能够处理
- 可靠性: 异常情况或者错误的处理策略是否明确,依赖的服务出现异常是否能够处理
- 简洁: 逻辑是否简单,后续调整功能或新增功能是否能够快速支持
- 清晰: 其他人在阅读理解代码的时候是否能清楚明白,重构或者修改功能是否不会担心出现无法预料的问题
代码格式
- 注释
- 命名规范
- 控制流程
- 错误和异常处理
自动格式化工具
gofmt
- Go语言官方提供的工具,能自动格式化Go语言代码为官方统一风格
goimports
- 也是Go语言官方提供的工具实际等于gofmt加上依赖包管理
注释
- 注释应该解释代码作用:适合注释公共符号
- 注释应该解释代码如何做的:适合注释实现过程
- 注释应该解释代码实现的原因:适合解释代码的外部因素、提供额外上下文
- 注释应该解释代码什么情况会出错:适合解释代码的限制条件
注意:公共符号始终要注释包中声明的每个公共的符号:
- 变量、常量、函数以及结构都需要添加注释
- 任何既不明显也不简短的公共功能必须予以注释
- 无论长度或复杂程度如何,对库中的任何函数都必须进行注释
命名规范
-
简洁胜于冗长
-
缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写·例如使用ServeHTTP而不是ServeHttp
·使用XMLHTTPRequest或者xmlHTTPRequest
-
变量距离其被使用的地方越远,则需要携带越多的上下文信息
·全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义
-
变量名有特定的含义
控制流程
- 线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支
- 正常流程代码沿着屏幕向下移动
- 提升代码可维护性和可读性
- 故障问题大多出现在复杂的条件语句和循环语句中
错误和异常处理
-
简单错误:
简单的错误指的是仅出现一次的错误,且在其他地方不需要捕获该错误优先使用errors.New来创建匿名变量来直接表示简单错误;如果有格式化的需求,使用fmt.Errorf
-
错误的Wrap和Unwrap:
错误的Wrap 实际上是提供了一个error嵌套另一个error 的能力,从而生成一个error的跟踪链;在fmt.Errorf中使用: %w关键字来将一个错误关联至错误链中
-
错误判定:
判定一个错误是否为特定错误,使用errors.ls;不同于使用==,使用该方法可以判定错误链上的所有错误是否含有特定的错误;在错误链上获取特定种类的错误,使用errors.As; error尽可能提供简明的上下文信息链,方便定位问题
-
panic:
不建议在业务代码中使用panic;调用函数不包含recover会造成程序崩溃若问题可以被屏蔽或解决,建议使用error代替panic;当程序启动阶段发生不可逆转的错误时,可以在init或main 函数中使用panic;panic用于真正异常的情况
-
recover: recover只能在被defer的函数中使用;嵌套无法生效;只在当前goroutine生效defer的语句是后进先出; recover生效范围,在当前goroutine的被defer 的函数中生效
性能优化建议
Go语言提供了支持基准性能测试的benchmark工具
go test -bench=. -benchmem
slice预分配内存
尽可能在使用make()初始化切片时提供容量信息
func NoPreAlloc( size int ) {
data := make( [ ]int,0)
for k := 0; k < size; k++ {
data = append( data,k )}
}
func PreAlloc(size int ) {
data := make( [ ]int,0, size)
for k := 0; k < size; k++ {
data = append( data,k)}
}
切片本质是一个数组片段的描述
-
包括数组指针
-
片段的长度
-
片段的容量(不改变内存分配情况下的最大长度)切片操作并不复制切片指向的元素
创建一个新的切片会复用原来切片的底层数组
type slice struct i
array unsafe.Pointerlen int
cap int
}
另一个陷阱:大内存未释放
在已有切片基础上创建切片,不会创建新的底层数组
场景
-
原切片较大,代码在原切片基础上新建小切片
-
原底层数组在内存中有引用,得不到释放
可使用copy替代re-slice
map预分配内存
不断向map中添加元素的操作会触发map的扩容
提前分配好空间可以减少内存拷贝和Rehash的消耗
建议根据实际需求提前预估好需要的空间
func PreAlloc( size int ) {
data := make( map[ int]int, size)for i := 0; i < size; i++ {
data[i] = 1
}
}
字符串处理
+ / strings.Builder / bytes.Buffer
使用+拼接性能最差,strings.Builder,bytes.Buffer 相近,strings.Buffer更快
分析
字符串在Go语言中是不可变类型,占用内存大小是固定的
使用+每次都会重新分配内存
strings.Builder,bytes.Buffer 底层都是[]byte 数组
bytes.Buffer 转化为字符串时重新申请了一块空间
strings.Builder 直接将底层的[]byte转换成了字符串类型返回
进一步优化 :Grow( n * len( str ) )
内存扩容策略,不需要每次拼接重新分配内存
空结构体
使用空结构体节省内存
空结构体struct(}实例不占据任何的内存空间可作为各种场景下的占位符使用
节省资源
空结构体本身具备很强的语义,即这里不需要任何值,仅作为占位符
使用atomic包
锁的实现是通过操作系统来实现,属于系统调用
atomic操作是通过硬件实现,效率比锁高
sync.Mutex应该用来保护一段逻辑,不仅仅用于保护一个变量
对于非数值操作,可以使用atomic.Value,能承载一个interface{}
性能调优
原则
- 要依靠数据不是猜测
- 要定位最大瓶颈而不是细枝末节
- 不要过早优化
- 不要过度优化
性能分析工具pprof
- 希望知道应用在什么地方耗费了多少CPU、Memory
- pprof是用于可视化和分析性能分析数据的工具
CPU
命令:topN
查看占用资源最多的函数
flat
当前函数本身的执行耗时
flat %
flat占CPU总时间的比例
sum %
上面每一行的flat%总和cum,指当前函数本身加上其调用函数的总耗时
cum %
cum占CPU总时间的比例
原理
操作系统
每10ms向进程发送一次SIGPROF信号
进程
每次接收到SIGPROF会记录调用堆栈
写缓冲
每100ms读取已经记录的调用栈并写入输
命令:list
根据指定的正则表达式查找代码行
命令: web
调用关系可视化
Heap-堆内存
- alloc_objects:程序累计申请的对象数
- inuse_objects :程序当前持有的对象数
- alloc_space:程序累计申请的内存大小
- inuse_space:程序当前占用的内存大小
原理
采样程序通过内存分配器在堆上分配和释放的内存,记录分配/释放的大小和数量
采样率:每分配512KB记录一次,可在运行开头修改,1为每次分配均记录
采样时间:从程序运行开始到采样时
采样指标: alloc_space, alloc_objects,inuse_space, inuse_objects
计算方式: inuse= alloc- free
goroutine-协程
goroutine泄露也会导致内存泄露
原理
Goroutine
·记录所有用户发起且在运行中的goroutine(即入口非runtime开头的)runtime.main的调用栈信息
ThreadCreate
·记录程序创建的所有系统线程的信息
pprof-采样过程
业务服务
基本概念
- 服务:能单独部署,承载一定功能的程序依赖:Service A的功能实现依赖
- ServiceB的响应结果,称为Service A依赖Service B
- 调用链路:能支持一个接口请求的相关服务集合及其相互之间的依赖关系
- 基础库:公共的工具包、中间件
流程
- 建立服务性能评估手段
- 分析性能数据,定位性能瓶颈
- 重点优化项改造
- 优化效果验证
建立服务性能评估手段
服务性能评估方式
- 单独benchmark无法满足复杂逻辑分析
- 不同负载情况下性能表现差异请求流量构造
- 不同请求参数覆盖逻辑不同
- 线上真实流量情况 压测范围
- 单机器压测
- 集群压测 性能数据采集
- 单机性能数据
- 集群性能数据
参考文献
[1] Go语言圣经(中文版) books.studygolang.com/gopl-zh/ch0…
[2] 青训营PPT