高质量代码 Profiling| 青训营笔记

112 阅读4分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天

什么是高质量代码

编写的代码正确可靠、简洁清晰。

  • 边界条件考虑完备

能够正确处理用户预期之外的输入。

  • 处理异常情况

服务或下游依赖出现意外,是否有明确的处理策略。

  • 易读、易维护

编程原则

简单性

  • 消除“多余的复杂性”,以简单清晰的逻辑编写代码

可读性

  • 编写可维护代码的第一步是确保代码可读

生产力

编码规范

  • 代码格式
  • 注释
  • 命名规范
  • 控制流程
  • 异常处理

代码格式

gofmt - 自动格式化代码

goimports 自动增删依赖的包引用,自动排序

注释

注释应该做的:

  • 解释代码的作用

  • 代码是如何做的

  • 实现的原因 适合解释代码的外部因素 提供额外的上下文 让后面来维护的人知道自己的改动会有怎样的影响

  • 代码什么时候会出错
    如对不规范的参数会有怎样的反应

代码是最好的注释

注释应该提供代码未表达出的上下文信息。

公共符号要注释。

命名规范

变量

  1. 简洁

  2. 缩略词全导出,若在开头且不想导出,使用全小写。 xmlHTTPRequest

  3. 变量定义距离使用的地方较远时,尽量在变量名中包含更多的上下文信息

函数

函数名不必携带上下文信息,以外包名和函数名总是成对出现。

当名为 foo 的包返回类型是 Foo 时,可以省略类型信息且不导致歧义。

当名为 foo 的包返回类型是 T 时(T 不是 Foo),可以在函数名中加入类型信息

只由小写字母组成,不包含大写字母和下划线。

简短并包含一定的上下文信息。

不要与标准库同名。

不使用常用变量名作为包名。

使用单数而不是复数。

谨慎地使用缩写,(保证使用缩写时不会有歧义)。

控制流程

处理逻辑尽量走直线,避免嵌套。

尽量保持代码路径为最小缩进。

优先处理错误情况,尽早返回或继续循环来减少嵌套。

异常处理

errors.Is 用来判断错误链中是否有某个错误。

errors.As 用来获取错误链中的某个错误。

若问题可以被屏蔽或解决,建议使用 error 代替 panic。

recover 只能在当前的 goroutine 生效,只能在被 defer 的函数中使用。

在 recover 时使用 debug.Stack() 打印调用栈,用于 debug。

性能优化建议

性能优化应该基于代码可读性和稳定性为前提。

依靠数据而不是猜测。

定位最大瓶颈,而不是细枝末节。

不要过早优化,不要过度优化。

预分配内存

尽可能在 make() 初始化 slicemap 时提供容量信息。

切片本质是一个数组片段的描述。

在原有切片上创建新切片时,不会释放原来的切片,如果只想保存部分数据,可以用 copy

字符串处理

字符串每次使用 + 都会重新创建内存。

Strings.Builder 的性能略优于 +

可以使用 builder.Grow() 预分配内存。

空结构体

struct{} 不占用任何的内存空间。

可以用 map[int]struct{} 这种方法来创建一个 Set。

使用 atomic

在多线程中如果想要操作临界资源,可以使用 atomic 包而不是 sync.Mutex

因为 atomic 包的锁是通过硬件实现, sync.Mutex 是通过软件实现。

性能分析工具 pprof

top - 查看占用时间最多的函数。

cum - 函数本身和函数内调用其他函数的时间总和。

web - 生成可视化的调用关系图。

heap - 堆内存

alloc_ vs inuse_: 累计 vs 正在使用中

采样过程和原理

CPU

操作系统每 10ms 向进程发送一次 SIGPROF 信号,

进程收到 SIGPROF 信号之后记录调用堆栈信息。

堆内存

通过记录堆上的分配和释放的内存,记录信息。

Goroutine & ThreadCreate

Stop The World -> 遍历链表/切片 -> 输出创建堆栈 -> Start The World

堵塞 & 锁

操作时上报调用栈和消耗时间,若超过阈值才会记录。

火焰图

由上到下表示调用顺序。

每块表示一个函数,越长代表占用 CPU 时间更长。