这是我参与「第三届青训营 -后端场」笔记创作活动的第3篇笔记
1. 编码规范
1.1 命名规范
-
for内部
-
方法签名,deadline是有特殊含义的,所以不能用t(泛指时间)代替,以免降低信息量
-
function
- 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的 (例如http包中的函数名Serve而不是ServeHTTP)
- 函数名尽量短
- 当名为foo的包某个函数返回类型Foo时,可以省略类型信息而不导致歧义
- 当名为foo的包某个韩式返回类型为T时(T并不是Foo),可以在函数名中加入类型信息
-
package
- 只由小写字母组成。不包含大写字母和下划线等字符
- 简短并包含一定的上下文信息。例如schema、task等
- 不要与标准库同名。例如不要使用sync或者strings
- 下列规则尽量满足
- 不使用常用变量名作为包名。例如使用bufio而不是buf
- 使用单数而不是复数。例如使用encoding而不是encodings
- 谨慎的使用缩写。例如使用fmt在不破坏上下文的情况下比format更加简短
-
小结
- 核心目标是降低阅读理解代码的成本
- 重点考虑上下文信息,设计简洁清晰的名称
1.2控制流程
-
避免嵌套,保持正常流程清晰
Bad:
if foo { return x } else { return nil }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 } -
小结
- 线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支
- 正常流程代码沿着屏幕向下移动
- 提升代码可维护性和可读性
- 故障问题大多出现在复杂的条件语句和循环语句中
1.3错误和异常处理
-
简单错误
- 简单的错误指的是仅出现一次的错误,且在其他地方不需要捕获该错误
- 优先使用error.New来创建匿名变量来直接表示简单错误
- 如果有格式化的需求,使用fmt.Errorf
-
错误的Wrap和Unwrap
- 错误的Wrap实际上是提供了一个error嵌套另一个error的能力,从而生成一个error的跟踪链
- 在fmt.Errorf中使用 %w 关键字来将一个错误关联至错误链中
- tip:Go1.13在errors中新增了三个新API和一个format关键字,分别是errors.Is 、errors.As 、errors.Unwrap以及fmt.Errorf的%w。如果项目在小于Go1.13的版本中,导入golang.org/x/xerrors来使用
-
错误判断
-
判定一个错误是否为特定错误,使用errors.Is
-
不同于使用==,使用该方法可以判定错误链上的所有错误是否含有特定的错误
-
在错误链上获取特定的某种错误,使用errors.As
if _, err := os.Open("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
- 调用函数不包含recover会造成程序崩溃
- 若问题可以被屏蔽或解决,建议使用error代替panic
- 当程序启动阶段发生不可逆转的错误时,可以在init或main函数中使用panic
-
recover
- recover只能在被defer的函数中使用
- 嵌套无法生效
- 只在当前goroutine生效
- defer的语句是后进先出
- 如果需要更多的上下文信息,可以再recover后在log中记录当前的调用栈
func (t *treeFs)Open(name string)(f fs.File, err error){ defer func(){ if e := recover(); e != nil { f = nil err = fmt.Errorf("gitfs panic: %v\n%s", e, debug.Stack()) } }() //... }
-
小结
- error尽可能提供简明的上下文信息链,方便定位问题
- panic用于真正异常的情况
- recover生效范围,在当前goroutine的被defer的函数中生效
2. 性能优化
2.1 Benchmark
-
示例,计算一个斐波那契数列来展示
package benchmark func Fib(n int) int { if n < 2 { return n } return Fib(n-1) + Fib(n-2) }package benchmark import "testing" func BenchmarkFib10(b *testing.B) { for n := 0; n < b.N; n++ { Fib(10) } }
2.2 slice预分配内存和引用
-
可以看出知道大小的情况下预分配的话,优化的空间是很大的,无论是时间还是空间上 (map预分配其实也是一样的)
-
陷阱:大内存未释放
- 在已有切片基础上创建切片,不会创建新的底层数组
- 场景
- 原切片较大,代码在原切片基础上新建小切片
- 原底层数组在内存中有引用,得不到释放
- 可使用copy替代re-silce
func GetLastBySlice(origin []int) []int { return origin[len(origin)-2:] } func GetLastByCopy(origin []int) []int { result := make([]int, 2) copy(result, origin[len(origin)-2:]) return result }//其中generateWithCap和printMem函数实现未知,先直接引用老师上课的结论,重要的是结果 func testGetLast(t *testing.T, f func([]int) []int) { result := make([][]int, 0) for k := 0; k < 100; k++ { origin := generateWithCap(128 * 1024) result = append(result, f(origin)) } printMem(t) _ = result }
2.3 字符串处理
-
使用 + 、strings.Builder 、bytes.Buffer 来分别处理拼接字符串效率比较
+处理最慢,strings.Builder和bytes.Buffer相近但是前者更快
- 分析:
- 字符串在Go语言中是不可变类型,占用内存大小是固定的
- 使用 + 每次都会重新分配内存
- strings.Builder,bytes.Buffer底层都是[]byte数组
- 内存扩容策略,不需要每次拼接重新分配内存
- strings.Builder比bytes.Buffer更快的原因:
- bytes.Buffer转化为字符串时重新申请了一块空间
- strings.Builder直接将底层的[]byte转换成了字符串类型返回
- 分析:
-
预分配
2.4 atomic包
- 锁的实现时通过操作系统来实现的,属于系统调用
- atomic操作时通过硬件实现,效率比锁高
- sync.Mutex应该用来保护一段逻辑,不仅仅用于保护一个变量
- 对于非数值操作,可以使用atomic.Value,能承载一个interface{}
3. 性能调优
3.1原则
- 要依靠数据而不是猜测
- 要定位最大瓶颈而不是细枝末节
- 不要过早优化
- 不要过度优化
3.2 pprof
-
class03中的main.go就是一个炸弹代码,专门用来熟悉pprof的使用
-
main函数代码
-
启动main.go之后,浏览器中输入127.0.0.1:6060/debug/pprof 得出以下界面
点击页内超链接可以看到相应的数据,但是比较的简洁,所以可以用一些可视化的工具来查看,需要下载一些依赖
-
或者终端输入 go tool pprof "http://localhost:6060/debug/pprof/profile?second=10"
根据报错提示安装依赖
sudo apt-get install graphviz最终网页结果
-
pprof中的top命令
- flat:当前函数本身的执行耗时
- flat%:flat占CPU总时间的比例
- sum%:上面每一行的flat%总和
- cum:指当前函数本身加上其调用函数的总耗时
- cum%:cum占CPU总时间的比例
- flat == cum ,函数中没有调用其他函数
- flat == 0,函数中只有其他函数的调用
-
list 命令
-
直接列出代码给出清晰的情况
-
-
查heap, go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
-
结果
-
导航栏:里面有一些火焰图和其他的可视化方式(goroutine、block、mutex、top、alloca、heap、source)
-
-
采样原理和过程
4. 业务服务优化
4.1 基本概念
- 服务:能单独部署,承载一定功能的程序
- 依赖:Service A的功能实现依赖Service B的响应结果,称为Service A依赖Service B
- 调用链路:能支持一个接口请求的相关服务集合及其相互之间的依赖关系(下图就是一个调用链路图)
- 基础库:公共的工具包、中间件
4.2 流程
-
建立服务性能评估手段 (Benchmark只能评估小部分,服务的话还是需要整体性的)
-
服务性能评估方式
- 单独Benchmark无法满足复杂逻辑分析
- 不同负载情况下性能表性差异
-
请求流量构造
- 不同请求参数覆盖逻辑不同
- 线上真实流量情况(压测平台模拟)
-
压测范围
- 单机器压测
- 集群压测
-
性能数据采集(pprof火焰图可行)
- 单机性能数据
- 集群性能数据
-
-
分析性能数据,定位性能瓶颈(可以通过火焰图看出)
- 使用库不规范
- 高并发场景优化不足(高峰期低峰期性能图判断,看看那些点是在高并发的时候优化不足的)
-
重点优化项改造
-
正确性是基础
-
响应数据diff
- 线上请求数据录制回放(先将之前的请求结果录制,优化完之后在与之前的结果相比较,来判断此次优化对正确性是否有影响)
- 新旧逻辑接口数据diff
-
-
优化效果验证
-
重复压测验证
-
上线评估优化效果
- 关注服务监控
- 逐步放量
- 收集性能数据
-
-
进一步优化,服务整体链路分析
- 规范上游服务调用接口,明确场景需求
- 分析链路,通过业务流程优化提升服务性能(一些重复调用的情况,需要根据特定的业务场景)
5. 基础库优化
5.1 AB实验SDK的优化
- 分析基础库核心逻辑和性能瓶颈
- 设计完善改造方案
- 数据按需获取
- 数据序列化协议优化
- 内部压测验证
- 推广业务服务落地验证
6. Go语言优化
6.1 编译器&运行时优化
- 优化内存分配策略
- 优化代码编译流程,生成更高效的程序
- 内部压测验证
- 推广业务服务落地验证
- 优点
- 介入简单,只需要调整编译配置
- 通用性强
7. 提问
-
遇到线上应用panic的时候应该怎么排查?分布式应用除了打log还有没有什么好的debug方式?
答:log给线上定位问题提供了便捷,因为一般都会有上下文调用堆栈,尤其是nil的情况。除了打log之外,还有go自带的debug工具,但是线上服务的时候不太方便开放,怕误操作,但是如果问题严重的话就可以从线上的机器中拿出一台来开放debug打断点去排查,但是还是很少用。