这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记。笔记内容包括我自己对课程内容的一些理解、对课程中代码的一些实践以及遇到的一些问题和bug
高质量编程
编程原则
- 简单性
- 可读性
- 生产力
编码规范
- 代码格式
- 注释
- 命名规范
- 控制流程
- 错误和异常处理
代码格式
- gofmt:go语言自动格式化工具
- goimports:gofmt加上依赖包管理
注释
- 代码作用
- 适合注释公共符号,比如open(),需要解释。
- 有的函数他的函数名就可以传递足够的信息,就不需要太多的注释
- 代码如何做的
- 实现过程
- 代码实现的原因
- 解释代码的外部因素
- 提供代码未表达的额外的上下文信息
- 代码什么情况下会出错
- 解释代码的限制条件
命名规范
variable:
- 简洁
- 缩略词全大写但当其位于变量开头而且不需要导出时,使用全小写
- 例如使用ServeHTTP而非ServeHttp。HTTP时缩略词要大写
- 使用XMLHTTPRequest或者xmlHTTPRequest。如果不需要导出,可以使用后一种
- 变量距离其被使用的地方越远,则需要携带更多的上下文信息
- 如全局变量名称需要更多上下文信息
//good
func (c *Clinet) send(req *Request,deadline time.Time)
//bad
function:
- 函数名不携带包名上下文信息
//good
func Serve(I net.Listener,handler Handler) error
//bad
func ServeHttp(I net.Listener,handler Handler) error
//函数名和报名往往成对出现(http.Serve),所以没必要在函数名中出现HTTP
- 函数名尽量简短
- 当名为foo的包某个函数返回类型Foo时,可以省略类型信息,不导致歧义
- 当名为foo的包某个函数返回类型T时,可以在函数名中加入类型信息。这个我不太理解,老师也没有举例
package:
必须满足:
- 只有小写字母
- 简短并包含一定上下文信息
- 不与标准库同名
尽量满足:
- 不适用常用变量名作为包名。如使用bufio而不是buf
- 使用单数而非复数。
- 谨慎使用缩写
控制流程
- 避免嵌套,保证正常流程清晰
//bad
if foo{
return x
} else{
return nil
}
//两个分支都有return,不需要else
//good
if foo{
return x
}
return nil
- 尽量保持正常代码路径为最小缩进
- 优先处理错误情况/特殊情况,尽早返回或者继续循环来减少嵌套
//bad
func OneFunc() error{
err:=doSomeThing()
if err == nil {
err:=doAnotherThing()
if err==nil{
return nil//正常返回
}
return err
}
return err
}
//good
func OneFunc() error{
if err:=doSomeThing();err != nil {
return err
}
if err:=doAnotherThing();err!=nil{
return err
}
return nil //正常返回
}
- 线性逻辑,处理逻辑尽量走直线,避免复杂的嵌套分支
- 正常代码沿着屏幕向下移动
- 提升代码可维护性和可读性
- 故障问题大多出现在复杂的条件语句和循环语句中3
错误和异常处理
简单错误:
- 仅出现一次的错误,在其他地方不需要捕获该错误
- 优先使用errors.New来创建匿名变量表示简单错误
- 如有格式化需求,使用fmt.Errorf
func defaultCheckRedirect(req *Request,via []*Request) error{
if len(via)>=10{
return errors.New("stopped after 10 redirects")
}
return nil
}
错误的Wrap和Unwrap
- 错误的Wrap实际上是提供了一个error嵌套另一个error的能力,从而生成了一个error跟踪链
- 在fmt.Errorf中使用%w关键字来讲一个错误关联至错误链中
list,_:=c.GetBytes(cache.Subkey(a.actionID,"srcfiles"))
if err!=nil{
return fmt.Errorf("reading srcfiles list:%w",err)
}
错误判定
- errors.Is:用于判断一个错误是否为特定错误,不同于==,该方法可以判断错误链上的所有错误是否包含特定的错误
data,err=lockedfile.Read(targ)
if errors.Is(err,fs.ErrNotExist){
if errors.Is(err,fs.ErrNotExist)
return []byte{},nil
}
return data,err
- errors.As:在错误链上获取特定种类的错误
if _,err:=os.Opne("non-existing");err!=nil{
var pathError *fs.PathError
if errors.As(err,&pathError){
fmt.Println("Failed at path:",pathError.Path)
}else{
fmt.Println(err)
}
}
- panic(运行时恐慌):是一种只会在程序运行时才回抛出来的异常。在panic被抛出之后,如果没有在程序里添加任何保护措施的话,程序就会在打印出panic的详情,终止运行。
- 不建议在业务代码中使用panic
- 调用函数不包含recover会造成程序崩溃
- 若问题可以被屏蔽或解决,建议使用error代替panic
- 当程序启动阶段发生不可逆转的错误时,可以在init或者main函数中使用panic
- recover:Recover 是一个Go语言的内建函数,可以让进入宕机流程中的 goroutine 恢复过来,recover 仅在延迟函数 defer 中有效,在正常的执行过程中,调用 recover 会返回 nil 并且没有其他任何效果,如果当前的 goroutine 陷入恐慌,调用 recover 可以捕获到 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);pk{
err=se.err
}else{
panic(e)
}
}
}()
}
- 如果需要更多的上下文信息,可以recover后再log中记录当前调用栈
性能优化建议
Benchmark
//fib.go
func Fib(n int) int {
if n < 2 {
return n
}
return Fib(n-1) + Fib(n-2)
}
//fib_test.go
func BenchmarkFib10(b *testing.B) {
for n := 0; n < b.N; n++ {
Fib(10)
}
}
//test
//输入命令行 go test -bench.= -benchmem
C:\lty\GO\workspace\bd\3\fib>go test -bench=. -benchmem
goos: windows
goarch: amd64
pkg: 3/fib
cpu: AMD Ryzen 7 5800H with Radeon Graphics
BenchmarkFib10-16 5251640 232.6 ns/op 0 B/op 0 allocs/op
PASS
ok 3/fib 1.490s
对于
BenchmarkFib10-16(测试函数名,-16表示GOMAXPROCS的值为16,默认为CPU核数)
5251640(一共执行的次数)
232.6 ns/op(每次花费的时间)
0 B/op(每次申请的内存)
0 allocs/op(每次执行申请几次内存)
Slice
Slice预分配内存
- 尽可能使用make()初始化切片时提供容量信息
//未指定容量大小
data:=make([]int,0)
//指定容量大小
data:=make([]int,0,size)
//后者比前者速度更快,内存分配次数更少
- 切片本质是一个数组片段的描述。如果预分配内存不够,添加元素会先进行一次扩容的操作再添加,因此影响性能
- 已有基础上创建切片,会引起大内存未释放的问题,可使用copy代替re-slice
//bad
func GetLastBySlice(origin []int) []int {
return origin[len(origin)-2:]
}
//good
func GetLastByCopy(origin []int) []int {
result := make([]int, 2)
copy(result, origin[len(origin)-2:])
return result
}
第一个方式占用的内存远大于第二种
Map
map预分配内存的情况和slice类似
//bad
data:=make(map[int]int)
//good
data:make(map[int]int,size)
//后者性能更好
向map种添加元素会触发map扩容,提前分配好空间可以减少内存拷贝和Rehash的消耗
字符串处理
三种字符串拼接方法:
- +
- strings.Builder
- bytes.Buffer
其中+最差,其他两个相近,strings.Builder更快(ppt写错了) 分析:
-
字符串是不可变类型,占用内存大小是固定的
-
使用+每次都会重新分配内存
-
strings.Builder,bytes.Buffer底层都是[]byte数组
-
内存扩容策略,不需要每次拼接重新分配内存
-
而bytes.Buffer转化字符串时重新申请了一块空间
-
strings.Builder直接将底层的[]byte转换成字符串类型返回,所以稍快 可采用预分配策略优化性能
func PreStrBuilder(n int, str string) string {
var builder strings.Builder
builder.Grow(n * len(str))
for i := 0; i < n; i++ {
builder.WriteString(str)
}
return builder.String()
}
func PreByteBuffer(n int, str string) string {
buf := new(bytes.Buffer)
buf.Grow(n * len(str))
for i := 0; i < n; i++ {
buf.WriteString(str)
}
return buf.String()
}
空结构体
使用空结构体节省内存:
- 空结构体struct{}不占据任何空间,可以做占位符使用
- 比如通过map实现set,只需要map的key,不需要value,可以通过struct{}作为占位符减少内存占用
func EmptyStructMap(n int){
m:=make(map[int]struct{})
for i:=0;i<n;i++{
m[i]=struct{}{}
}
}
func BoolMap(n int){
m:=make(map[int]bool)
for i:=0;i<n;i++{
m[i]=False
}
}
//前者的内存占用小于后者,因为即使是bool类型也要占用1个字节的空间
atomic包
type atomicCounter struct{
i int32
}
func AtomicAddOne(c *atomicCounter){
atomic.AddInt32(&c.i,1)
}
//----------------
type mutexCounter struct{
i int32
m sync.Mutex
}
func MutexAddOne(c *mutexCounter){
c.m.Lock()
c.i++
c.m.Unlock
}
//前者的性能高于后者
原因:
- 锁是通过操作系统实现的,属于系统调用
- atomic操作是通过硬件实现的,效率比锁高
- sync,Mutex应该用来保护一段逻辑,不仅仅用于保护一个变量
- 对于非数值操作,可以使用atomic.Value,能承载一个interface{}
性能调优实战
性能分析工具pprof
func main() {
log.SetFlags(log.Lshortfile | log.LstdFlags)
log.SetOutput(os.Stdout)
runtime.GOMAXPROCS(1) //限制cpu使用数
runtime.SetMutexProfileFraction(1) //开启锁调用跟踪
runtime.SetBlockProfileRate(1) //开启阻塞调用跟踪
go func() {
if err := http.ListenAndServe(":6060", nil); err != nil {
log.Fatal(err)
}
os.Exit(0)
}()
//……
}
pprof-排查实战
CPU
go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"
seconds=10代表采集10s
命令top
(pprof) top
Showing nodes accounting for 3.38s, 97.41% of 3.47s total
Dropped 31 nodes (cum <= 0.02s)
Showing top 10 nodes out of 15
flat flat% sum% cum cum%
3.19s 91.93% 91.93% 3.19s 91.93% github.com/wolfogre/go-pprof-practice/animal/felidae/tiger.(*Tiger).Eat
0.18s 5.19% 97.12% 0.18s 5.19% runtime.stdcall3
0.01s 0.29% 97.41% 0.04s 1.15% runtime.bgscavenge
0 0% 97.41% 0.02s 0.58% github.com/wolfogre/go-pprof-practice/animal/canidae/dog.(*Dog).Eat (inline)
0 0% 97.41% 0.02s 0.58% github.com/wolfogre/go-pprof-practice/animal/canidae/dog.(*Dog).Live
0 0% 97.41% 3.19s 91.93% github.com/wolfogre/go-pprof-practice/animal/felidae/tiger.(*Tiger).Live
0 0% 97.41% 3.22s 92.80% main.main
0 0% 97.41% 0.02s 0.58% runtime.(*pageAlloc).scavenge
0 0% 97.41% 0.20s 5.76% runtime.(*pageAlloc).scavenge.func1
0 0% 97.41% 0.20s 5.76% runtime.(*pageAlloc).scavengeOne
flat: 当前函数本身的执行耗时
flat%: flat占CPU总时间的比例
sum%: 上面一行的flat%总和
cum: 指当前函数本身加上其调用函数的总耗时
cum%: cum占CPU总时间的比例
flat==cum:函数中没有调用其他函数
flat==0:函数中只有其他函数的调用
命令list(+参数):
根据正则表达式查找代码行
(pprof) list Eat
Total: 3.47s
ROUTINE ======================== github.com/wolfogre/go-pprof-practice/animal/canidae/dog.(*Dog).Eat in C:\lty\GO\pkg\mod\github.com\wolfogre\go-pprof-practice@v0.0.0-20190402114113-8ce266a210ee\animal\canidae\dog\dog.go
0 20ms (flat, cum) 0.58% of Total
. . 21: d.Run()
. . 22: d.Howl()
. . 23:}
. . 24:
. . 25:func (d *Dog) Eat() {
. 20ms 26: log.Println(d.Name(), "eat")
. . 27:}
. . 28:
. . 29:func (d *Dog) Drink() {
. . 30: log.Println(d.Name(), "drink")
. . 31:}
ROUTINE ======================== github.com/wolfogre/go-pprof-practice/animal/felidae/tiger.(*Tiger).Eat in C:\lty\GO\pkg\mod\github.com\wolfogre\go-pprof-practice@v0.0.0-20190402114113-8ce266a210ee\animal\felidae\tiger\tiger.go
3.19s 3.19s (flat, cum) 91.93% of Total
. . 19:}
. . 20:
. . 21:func (t *Tiger) Eat() {
. . 22: log.Println(t.Name(), "eat")
. . 23: loop := 10000000000
3.19s 3.19s 24: for i := 0; i < loop; i++ {
. . 25: // do nothing
. . 26: }
. . 27:}
. . 28:
. . 29:func (t *Tiger) Drink() {
(pprof)
可以看出CPU性能消耗最大的部分再第24行
命令web
调用关系可视化 输入该命令可能会报错,是因为没有安装Graphviz导致,安装地址:graphviz.org/download
可见tiger.Eat占用CPU最多,我们animal/felidae/tiger/tiger.go中Live接口中的Eat方法已经定义的Eat方法都注释掉,再次查看
func (t *Tiger) Eat() {
log.Println(t.Name(), "eat")
// log.Println(t.Name(), "eat")
// loop := 10000000000
// for i := 0; i < loop; i++ {
// // do nothing
// }
}
cpu的使用已经被稀释掉了
此处遇到一个问题:改动tiger后并没有发生变化,原因是没有改变引用包,main使用的tiger是github仓库中的而非本地的。修改所有import为本地内容后解决
Heap-堆内存
输入命令
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
添加参数-http:8080后,命令行会直接打开浏览器查看可视化图
可见mouse Steal占用了大量的内存
同样,我们修改mouse.Steal方法
func (m *Mouse) Steal() {
log.Println(m.Name(), "steal")
// max := constant.Gi
// for len(m.buffer)*constant.Mi < max {
// m.buffer = append(m.buffer, [constant.Mi]byte{})
// }
}
问题解决,内存占用得到稀释
之后查看sample->alloc space视图
发现Dog.run()分配了大量内存,将其内容注释掉
func (d *Dog) Run() {
log.Println(d.Name(), "run")
// _ = make([]byte, 16*constant.Mi)
}
问题解决
goroutine-协程
goroutine的泄露也会导致协程的泄露 输入命令
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"
通过view-flame graph,查看火焰图 从上到下表示顺序,每一个小块表示函数,小块的长度代表占用cpu的时间
wolf.drink()占用cpu时间较长,我们修改该方法
func (w *Wolf) Drink() {
log.Println(w.Name(), "drink")
// for i := 0; i < 10; i++ {
// go func() {
// time.Sleep(30 * time.Second)
// }()
// }
}
问题解决
mutex-锁
都差不多,改一下后缀
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"
在http://localhost:8080/ui/source 中查看有问题的代码
修改代码
func (w *Wolf) Howl() {
log.Println(w.Name(), "howl")
m := &sync.Mutex{}
m.Lock()
go func() {
time.Sleep(time.Second)
// m.Unlock()
}()
m.Lock()
}
重新运行
ok
block-阻塞
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"
我这里似乎没有ppt上展示的问题
首页的block也没有cat相关的block
暂且不搞了,流程都差不多
pprof采样过程及原理
CPU
- 采样对象:函数调用和它们占用的时间
- 采样率:100次/秒,固定值
- 采样时间:从手动启动到手动结束
流程:
开始采样 -> 设定信号处理器 -> 开启定时器
停止采样 -> 取消信号处理函数 -> 关闭定时器
heap-堆内存
- 采样程序通过内存分配器在堆上分配和释放内存,记录分配/释放的大小和数量
- 每分配512KB记录一次,可在运行开头修改,1为每次分配均记录
- 采样时间:从程序开始到采样时
- 采样指标:alloc_space,alloc_object,inuse_space,inuse_object
- 计算方式:inuse=alloc-free
Goroutine-协程 & ThreadCreate-线程创建
- Goroutine:
- 记录所有用户发起且在运行中的goroutine(即入口非runtime开头的)
- runtime.main的调用栈信息
- ThreadCreate:
- 记录程序创建的所有系统线程的信息
Block-阻塞 &Mutex-锁
- 阻塞:
- 采样阻塞操作的次数和耗时
- 采样率:阻塞耗时超过阈值才会被记录
- 锁竞争
- 采样争抢锁的次数和耗时
- 采样率:指记录固定比例的锁操作,1为每次加锁均记录
性能调优案例
业务服务优化
流程:
- 建立服务性能评估手段
- 分析性能数据,定位性能瓶颈
- 重点优化项改造
- 优化效果验证
后面的内容没有什么实践性的内容,我对这些也不是太理解,就没有继续记了。可以看ppt