这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天
什么是高质量代码
编写的代码正确可靠、简洁清晰。
- 边界条件考虑完备
能够正确处理用户预期之外的输入。
- 处理异常情况
服务或下游依赖出现意外,是否有明确的处理策略。
- 易读、易维护
编程原则
简单性
- 消除“多余的复杂性”,以简单清晰的逻辑编写代码
可读性
- 编写可维护代码的第一步是确保代码可读
生产力
编码规范
- 代码格式
- 注释
- 命名规范
- 控制流程
- 异常处理
代码格式
gofmt - 自动格式化代码
goimports 自动增删依赖的包引用,自动排序
注释
注释应该做的:
-
解释代码的作用
-
代码是如何做的
-
实现的原因 适合解释代码的外部因素 提供额外的上下文 让后面来维护的人知道自己的改动会有怎样的影响
-
代码什么时候会出错
如对不规范的参数会有怎样的反应
代码是最好的注释
注释应该提供代码未表达出的上下文信息。
公共符号要注释。
命名规范
变量
-
简洁
-
缩略词全导出,若在开头且不想导出,使用全小写。
xmlHTTPRequest -
变量定义距离使用的地方较远时,尽量在变量名中包含更多的上下文信息
函数
函数名不必携带上下文信息,以外包名和函数名总是成对出现。
当名为 foo 的包返回类型是 Foo 时,可以省略类型信息且不导致歧义。
当名为 foo 的包返回类型是 T 时(T 不是 Foo),可以在函数名中加入类型信息
包
只由小写字母组成,不包含大写字母和下划线。
简短并包含一定的上下文信息。
不要与标准库同名。
不使用常用变量名作为包名。
使用单数而不是复数。
谨慎地使用缩写,(保证使用缩写时不会有歧义)。
控制流程
处理逻辑尽量走直线,避免嵌套。
尽量保持代码路径为最小缩进。
优先处理错误情况,尽早返回或继续循环来减少嵌套。
异常处理
errors.Is 用来判断错误链中是否有某个错误。
errors.As 用来获取错误链中的某个错误。
若问题可以被屏蔽或解决,建议使用 error 代替 panic。
recover 只能在当前的 goroutine 生效,只能在被 defer 的函数中使用。
在 recover 时使用 debug.Stack() 打印调用栈,用于 debug。
性能优化建议
性能优化应该基于代码可读性和稳定性为前提。
依靠数据而不是猜测。
定位最大瓶颈,而不是细枝末节。
不要过早优化,不要过度优化。
预分配内存
尽可能在 make() 初始化 slice 和 map 时提供容量信息。
切片本质是一个数组片段的描述。
在原有切片上创建新切片时,不会释放原来的切片,如果只想保存部分数据,可以用 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 时间更长。