这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天
高质量编程与性能调优
高质量标准:
- 正确可靠、简介清晰
- 边界条件考虑完备
- 异常情况处理、稳定性保证
- 易读易维护
注释
公共符号注释
- 变量、常量、函数、结构
- 任何函数
- 不需要注释实现接口的方法
注释使用:
- 解释代码作用
- 代码如何做
- 代码实现原因
- 代码什么时候会出错
代码格式
使用gofmt自动格式化代码:goLand内置工具
goimports
命名规范
变量
- 简洁
- 缩略全大写:如ServeGTTP
- 全局变量多带上下文信息
函数参数
- 如时间变量
deadline
函数命名
- 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现
- 尽量简短
- 当名为foo的包某个函数返回类型Foo时,开源省略类型信息
- 当名为foo的包某个函数返回类型T时,可以在函数名中加入类型信息
包命名
- 只由小写组成
- 包含上下文信息
- 不要与标准库同名:sync、strings等
- 不要用常用变量名作包名:使用bufio而不是buf
- 使用单数不是复数
控制流程
- 避免嵌套、保证正常流程:如都包含return去掉多余的else
- 优先处理错误情况/特殊情况,尽早返回或继续循环来减少嵌套
错误和异常处理
简单错误:仅出现一次的错误
- 优先使用errors.New来创建匿名变量来直接表述简单错误
- 如果有格式化的需求,使用fmt.Errorf
复杂错误
- 错误的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)
}
错误判定:
- 使用
errors.Is判断错误为特定错误 - 在错误链上获取特定类的错误:
errors.As
var pathError *fs.PathError
if errors.As(err,&pathError){
fmt.Println("Failed at path:",pathError.Path)
}else{
fmt.Println(err)
}
panic
- 不建议在业务代码中使用panic
- 调用函数不包含recover会造成程序崩溃
- 若问题可以被屏蔽或解决,建议使用error代替panic
- 当程序启动阶段发生不可逆转的错误时,可以在init或main函数中使用panic
recover
- 只能在被defer的函数中使用
- 嵌套无法生效
- 只在当前goroutine生效
- defer的语句是后进先出
func(s *ss) Token(skipSpace bool,f func(rune) bool)(tok []byte,err error){
defer func() {
if e:= recover(); e!=nil{
if se,ok := e.(scanError); ok{
err = se.err
}else {
panic(e)
}
}
}()
}
- 在log中记录当前的调用栈
err = fmt.Errorf("gitfs panic: %v\n%s",e,debug.Stack())
性能优化
-
slice预分配内存
- 尽可能在使用make()初始化切片时提供容量信息
- 切片本质是数组片段:包括数组指针、片段长度、容量
-
map预分配内存
-
字符串处理
- 使用strings.Builder
-
builder.WriteString() - 字符串在go语言中是不可变类型,占用内存大小是固定的
- 使用+每次都会重新分配内存
- 底层是[]byte数组
- 内存扩容策略,不需要每次拼接重新分配内存
tips:
bytes.Buffer转化为字符串时重新申请了一块空间,strings.Builder直接将底层的[]byte转换成了字符串
-
空结构体节省内存
- 实现Set,可以考虑用map代替
-
使用atomic包
-
func AtomicAddOne(c *atomicCounter){ atomic.AddInt32(&c,i,1) //实现加锁解锁 } -
func MutexAddOne(c *mutexCounter){ c.m.Lock() c.i++ c.m.Unlock() } - 锁的实现是通过操作系统来实现,属于系统调用
- atomic操作是通过硬件实现,效率比锁高
- sync.Mutex应该用来保护一段逻辑,不仅仅用于保护一个变量
- 对于非数值操作,可以使用atomic.Value能承载一个interface{}
-
性能分析工具
pprof是用于可视化和分析性能分析数据的工具
功能简介
实践项目地址:github.com/wolfogre/go…
1.导出数据
网页:在import中加上
"net/http"
_"net/http/pprof"
代码运行地方
go func() {
log.Println(http.ListAndServe(":6060",nil))
}()
看到nlock和mutex的信息,在代码中加上
runtime.SetBlockProfileRate(1)
runtime.SetMutexProfileFraction(1)
下载信息
方法一:直接在终端运行go tool pprof http://localhost:6060/debug/pprof/XXX
方法二:打开网址:(http://localhost:6060/debug/pprof/XXX)
| allocs | 内存分配情况的采样信息 | 可以用浏览器打开,但可读性不高 |
|---|---|---|
| blocks | 阻塞操作情况的采样信息 | 可以用浏览器打开,但可读性不高 |
| cmdline | 显示程序启动命令及参数 | 可以用浏览器打开,这里会显示 ./go-pprof-practice |
| goroutine | 当前所有协程的堆栈信息 | 可以用浏览器打开,但可读性不高 |
| heap | 堆上内存使用情况的采样信息 | 可以用浏览器打开,但可读性不高 |
| mutex | 锁争用情况的采样信息 | 可以用浏览器打开,但可读性不高 |
| profile | CPU 占用情况的采样信息 | 浏览器打开会下载文件 |
| threadcreate | 系统线程创建情况的采样信息 | 可以用浏览器打开,但可读性不高 |
| trace | 程序运行跟踪信息 | 浏览器打开会下载文件 |
分析数据
可以通过任务管理器查看CPU情况,
在交互式终端中使用命令
go tool pprof http://localhost:6060/debug/pprof/profile
输入top命令查看CPU较高的调用:消耗前10的函数
输入list Eat查看问题具体在代码哪里
启动web服务器并自动打开一个网页:
go tool pprof -http=:8000 http://6060/debug/pprof/profile
图形化调用graphviz,需要下载安装!!
采样过程和原理
CPU
- 操作系统,每10ms向进程发送一次信号
- 进程:每次就收到信号都会记录调用堆栈
- 写缓冲:每100ms读取已经记录的调用栈并写入输出流
堆内存
- 通过内存分配器在堆上分配和释放内存
- 每分配512K记录一次
- 采样时间从程序运行开始到采样
- 采样指标:alloc_space,alloc_objects,inuse_space,inuse_objects
- 计算方式:inuse = alloc-free
Goroutine和ThreadCreate线程创建
Goroutine
- 记录所有用户发起且在运行中的goroutine
- runtime.main的调用栈信息
ThreadCreate
- 记录程序创建的所有系统线程的信息
Block阻塞
阻塞操作
- 采样阻塞操作的次数和耗时
- 采样率:阻塞耗时超时阈值的才被记录,1为每次阻塞均记录
Mutex锁
锁竞争
- 采样争抢锁的次数和耗时
- 采样率:只记录固定比例的锁操作,1为每次加锁均记录
Go语言优化
业务层优化:
- 针对特定场景、具体问题、具体分析
- 容易获得较大性能收益
语言运行时优化:
- 解决更通用的性能问题
- 考虑更多场景
- Tradeoffs
数据驱动
- 自动化性能分析工具-pprof
- 依靠数据
- 首先优化最大瓶颈
自动内存管理
动态内存
-
程序在运行时根据需求动态分配内存:malloc()
-
自动内存管理(垃圾回收):由程序语言的运行时系统管理动态内存
- 避免手动管理,专注于实现业务逻辑
- 保证内存使用的正确性和安全性:double-free、use-after-free
概念
- Mutator:业务线程,分配新对象
- Collector:GC线程,找到存活对象,回收死亡的内存空间
- Serial GC:只有一个collector
- Parallel GC:支持多个collectors同时回收GC
- Concurrent GC:mutator和collector可以同时执行
追踪垃圾回收
-
对象被回收的条件:指针指向关系不可达的对象
-
标记根对象:静态变量、全局变量、常量、线程栈等
-
标记:找到可达对象:求指针指向关系的传递闭包:从根对象出发找到所有可达对象
-
清理:所有不可达对象
- 将存活对象复制到另外的内存空间(Copying GC)
- 将死亡对象的内存标记为“可分配”(Mark-sweep GC)
- 移动并整理存活对象(Mark-compact GC)
分代GC
-
年轻代
- 常规的对象分配
- 存活对象很少,用copying collection
- GC吞吐率高
-
老年代
- 对象趋向于一直存活,反复复制开销大
- 可以采用mark-sweep collection
引用计数
- 每个对象都有一个与之关联的引用数目
- 对象存活的条件:当且仅当引用数大于0
优点:
- 内存管理的操作分担到程序执行过程中
- 不需要了解runtime的实现细节
缺点:
- 维护开销大:原子操作保证对引用技术操作的原子性和可见性
- 无法回收环形数据结构 weak reference
- 内存开销:每个对象都引入额外的内存空间存储引用数目
- 回收内存依然可能引发暂停
内存分配
- 目标:对象在heap上分配内存
- 提前内存分块
- 对象分配:根据对象的大小选择最合适的块返回
缓存
- TCMalloc:thread caching
- 每个p包含一个mcache用于快速分配,为绑定p上的g分配对象
- mcache管理一组mspan
- 当mcache中的mspan分配完毕,向mcentral申请带有未分配块的mspan
- 当mspan中没有分配对象,mspan会被缓存在mcentral中,不是立刻释放归还给OS
编译器优化
结构
系统软件
- 识别符合语法和非法的程序
- 生成正确且高效的代码
分析部分
- 词法分析
- 语法分析
- 语义分析
- 中间代码生成
综合部分
- 代码优化
- 代码生成
静态分析
不执行程序代码,推导程序的行为,分析程序的性质
控制流:程序执行的流程
数据流:数据在控制流上的传递