这是我参与「第三届青训营 -后端场」笔记创作活动的第3篇笔记
高质量编程
简介
- 编写的代码能够达到正确可靠、简洁清晰的目标可称之为高质量代码
- 各种边界条件是否考虑完备
- 异常情况处理,稳定性保证
- 易读易维护
编程原则
简单性
- 消除“多余的复杂性”,以简单清晰的逻辑写代码
- 不理解的代码无法修复改进
可读性
- 代码是写给人看的,而不是机器
- 编写可维护代码的第一步是确保代码可读性
生产力
- 团队整体工作效率非常重要
编程规范
代码格式
注释
-
解释代码的作用
-
解释代码如何做
-
注释应该提醒使用者一些潜在的限制条件或者会无法处理的情况
-
公共符号始终要注释
变量起名
-
简洁胜于冗长
-
缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
- 例如:ServeHTTP而不是ServeHttp
- 使用XMLHTTPRequest或者xmlHTTPRequest
函数起名
- 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的
- 函数名尽量简短
包起名
- 只由小写字母组成。不包含大写字母和下划线等字符
- 简短并包含一定的上下文信息。例如 schema、task 等
- 不要与标准库同名。例如不要使用 sync 或者 strings
控制流程
- 避免嵌套,保持正常流程清晰
- 如果两个分支中都包含 return 语句,则可以去除冗余的 else
if xxx {
return xxx
}
return xxx
错误和异常处理
简单错误处理
- 优先使用 errors.New 来创建匿名变量来直接表示该错误。有格式化需求时使用 fmt.Errorf
- 处理方式称为“不透明错误处理”
复杂的错误处理
通过阅读源码 /usr/local/go/src/errors/wrap.go,我们可以发现 As() 和 Is() 是通过在错误链中不断调用 Unwrap() 函数,最终找到匹配的错误值。其中,Unwrap() 函数也是在 golang 1.13 中新增的函数。
- 错误链参考文章:blog.csdn.net/EDDYCJY/art…
- 在 fmt.Errorf 中使用 %w 关键字来将一个错误 wrap 至其错误链中
错误链上判断
- 使用 errors.Is 可以判定错误链上的所有错误是否含有特定的错误。
package main
import (
"errors"
"fmt"
)
// 哨兵错误处理
var (
ErrInvalidUser = errors.New("invalid user")
ErrNotFoundUser = errors.New("not found user")
)
func main() {
err1 := fmt.Errorf("wrap err1: %w\n", ErrInvalidUser)
err2 := fmt.Errorf("wrap err2: %w\n", err1)
// golang 1.13 新增 Is() 函数
if errors.Is(err2, ErrInvalidUser) {
fmt.Println(ErrInvalidUser)
return
}
fmt.Println("success")
}
错误链上获取
- 在错误链上获取特定种类的错误,使用 errors.As
package main
import (
"errors"
"fmt"
)
// DefineError 自定义的错误类型
type DefineError struct {
msg string
}
func (d *DefineError) Error() string {
return d.msg
}
func main() {
// wrap error,错误链形成
err1 := &DefineError{"this is a define error type"}
err2 := fmt.Errorf("wrap err2: %w\n", err1)
err3 := fmt.Errorf("wrap err3: %w\n", err2)
var err4 *DefineError
if errors.As(err3, &err4) {
// errors.As() 顺着错误链,从 err3 一直找到被包装最底层的错误值 err1,并且将 err3 与其自定义类型 `var err4 *DefineError` 匹配成功。
fmt.Println("err1 is a variable of the DefineError type")
fmt.Println(err4 == err1)
return
}
fmt.Println("err1 is not a variable of the DefineError type")
}
还有一个errors.As方法,它和is的区别在于as会提取出调用链中指定类型的错误,并将错误赋值给定义好的变量,方便后续处理
panic
- 不建议在业务代码中使用 panic
- 如果当前 goroutine 中所有 deferred 函数都不包含 recover 就会造成整个程序崩溃
- 当程序启动阶段发生不可逆转的错误时,可以在 init 或 main 函数中使用 panic
recover
- recover 只能在被 defer 的函数中使用,嵌套无法生效,只在当前 goroutine 生效
- 如果需要更多的上下文信息,可以 recover 后在 log 中记录当前的调用栈。
defer func() {
if e := recover(); e != nil {
f = nil
err = fmt.Errorf("gitfs panic: %v\n%s", e, debug.Stack())
}
}()
性能优化建议
工具
- 在满足正确性、可靠性、健壮性、可读性等质量因素的前提下,设法提高程序的效率
package test
func Fib(n int) int {
if n < 2 {
return n
}
return Fib(n-1)+Fib(n-2)
}
package test
import "testing"
func BenchmarkFib(b *testing.B) {
for i := 0; i < b.N; i++ {
Fib(10)
}
}
- go test -v -bench="." -benchmem,windows要加".",mac电脑不需要加这个双引号
slice预分配内存
-
尽可能使用make()初始化切片时提供容量信息
- make(Type, len, cap)
切片本质是一个数组片段的描述 包括数组指针 片段的长度 片段的容量(不改变内存分配情况下的最大长度) 切片操作并不复制切片指向的元素 创建一个新的切片会复用原来切片的底层数组 以切片的append 为例,append时有两种场景: 当 append 之后的长度小于等于 cap,将会直接利用原底层数组剩余的空间。 当 append 后的长度大于 cap 时,则会分配一块更大的区域来容纳新的底层数组。 因此,为了避免内存发生拷贝,如果能够知道最终的切片的大小,预先设置 cap 的值能够避免额外的内存分配,获得更好的性能
内存大坑
func TestGetLastBySlice(t *testing.T) {
m := GetLastBySlice(make([]int,1000,1000))
fmt.Println(m)
}
func TestGetLastByCopy(t *testing.T) {
m := GetLastByCopy(make([]int,1000,1000))
fmt.Println(m)
}
了解slice的基本结构之后,还有个问题需要注意 因此很可能出现这么一种情况,原切片由大量的元素构成,但是我们在原切片的基础上切片,虽然只使用了很小一段,但底层数组在内存中仍然占据了大量空间,得不到释放 可使用 copy 替代 re-slice 两部分代码使用了不同的逻辑取slice的最后两位数创建新数组,同时统计输出了内存占用信息 结果差异非常明显,lastBySlice 耗费了 100.14 MB 内存,也就是说,申请的 100 个 1 MB 大小的内存没有被回收。因为切片虽然只使用了最后 2 个元素,但是因为与原来 1M 的切片引用了相同的底层数组,底层数组得不到释放,因此,最终 100 MB 的内存始终得不到释放。而 lastByCopy 仅消耗了 3.14 MB 的内存。这是因为,通过 copy,指向了一个新的底层数组,当 origin 不再被引用后,内存会被垃圾回收
map建议
-
预分配内存
- make(map[type]type,size)
- 提前分配好空间可以减少内存拷贝和Rehash的消耗
字符串拼接
-
使用"+"拼接性能最差
- 每次都会重新分配内存
-
strings.Builder、strings.Buffer相近,strings.Buffer更快
-
内存扩容策略,不需要每次拼接重新分配内存
-
bytes.Buffer转化为字符串时重新申请了一块内存
-
strings.Buffer直接将底层的[]byte转换成了字符串类型返回
- 我们使用Grow(int)预分配一下内存,这样速度更快
-
package main
import (
"bytes"
"fmt"
"strings"
)
func main() {
t1()
t2()
t3()
}
func t1() {
var s strings.Builder
for i := 0; i < 10; i++ {
s.WriteString("1")
}
fmt.Println(s.String())
}
func t2() {
var s bytes.Buffer
for i := 0; i < 10; i++ {
s.WriteString("1")
}
fmt.Println(s.String())
}
func t3() {
var s strings.Builder
s.Grow(10)
for i := 0; i < 10; i++ {
s.WriteString("1")
}
fmt.Println(s.String())
}
空结构体
atomic包
- atomic操作通过硬件实现,效率比锁高
- 对应非数值操作,可以使用atomic.Value,能承载一个interface{}
- 参考文档地址:点击跳转
package main
import (
"fmt"
"sync"
"sync/atomic"
)
var (
x int64
wg sync.WaitGroup
)
func main() {
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
atomic.AddInt64(&x,10)
wg.Done()
}()
}
wg.Wait()
fmt.Println(x)
}
性能调优实战
简介
优化原则
要依靠数据不是猜测 要定位最大瓶颈而不是细枝末节 不要过早优化 不要过度优化
工具:pprof
简介
应用在什么地方耗费了多少CPU、Memory
在浏览器中打开http://localhost:6060/debug/pprof,可以看到这样的页面, 这就是我们刚刚引入的net/http/pprof注入的入口了。 页面上展示了可用的程序运行采样数据,下面也有简单说明,分别是: allocs:内存分配情况 blocks:阻塞操作情况 cmdline:当前程序的命令行的完整调用路径。 goroutine:当前所有goroutine的堆栈信息 heap:堆上内存使用情况(同alloc) mutex:锁竞争操作情况 profile: CPU占用情况 threadcreate:当前所有创建的系统线程的堆栈信息 trace:程序运行跟踪信息
排查CPU
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=10 再输入top
- flat == cum,函数中没有调用其他函数
- fat == 0,函数中只有其他函数的调用
list 文件名:eg:list Eat
- 这样我们就能找到问题所在
输入web命令,调用关系图可视化
排查heap
- go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
注意,要显示这个我们还需要安装一个东西,Graphviz
- 通过view那里选择Top、Source我们就可以定位到我们内存出大问题的地方
sample那里
排查goroutine
- http://localhost:6060/debug/pprof/goroutine?debug=1
- go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"
切换到火焰图
打开View菜单,切换到Flame Graph视图 可以看到,刚才的节点被堆叠了起来 图中,自顶向下展示了各个调用,表示各个函数调用之间的层级关系 每一行中,条形越长代表消耗的资源占比越多 显然,那些「又平又长」的节点是占用资源多的节点 可以看到,*Wolf.Drink()这个调用创建了超过90%的goroutine,问题可能在这里 火焰图是非常常用的性能分析工具,在程序逻辑复杂的情况下很有用,可以重点熟悉
排查锁mutex
- go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"
排查阻塞block
- go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"
过程:pprof
CPU
- 采样对象:函数调用和它们占用的时间
- 采样率:100次/秒,固定值
- 采样时间:从手动启动到手动结束
goroutine
Heap
Block
- 锁竞争
- 采样争抢锁的次数和耗时
- 采样率:只记录固定比例的锁操作,1为每次加锁均记录
Mutex
- 采样阻塞操作的次数和耗时
- 采样率:阻塞耗时超过阈值才会被记录,1为每次阻塞均记录
优化案例
步骤
- 建立服务性能评估手段
- 分析性能数据,定位性能瓶颈
- 重点优化项改造
- 优化效果验证