正确性是基础,而高质量是追求。
上面这句话显然是不言而喻的。那么问题来了,我们要追求高质量的编程,而究竟何谓高质量的代码,又有什么注意规范?这篇笔记将根据青训营课程中高质量编程的课程,结合自身的经历与感悟,为大家总结梳理一下常见的高质量编程规范与要点。
高质量编程概览
正如我们开篇所给出的一样,一言以蔽之,高质量编程就是指我们编写的代码能够达到正确可靠、简洁清晰的目标。究其本身,又有几个要点:
- 各种边界情况是否考虑完备
- 异常情况处理,稳定性保证
- 易读易维护
边界情况
提到对于边界情况的警惕,在初学编程的过程中遇到最多的情况就是各种越界问题。就拿C++举例,如果使用vector数组越界好在还会发出错误警告提醒你“有地方越界了,快去修bug!”。但更不巧的是如果使用传统C数组,越界情况不会被告知,如果恰巧修改到某个危险区域,就可能引发程序崩溃等严重问题。
一个更加经典的例子就是C++的gets()函数被弃用并删除,因为其机制原因,不检查并限制输入长度可能会使缓冲区边界溢出,引发安全漏洞。
异常情况
这就又涉及到一个经典的程序员笑话:酒吧炒饭问题。
酒吧本应该是点酒品的场所,但当面对点各种稀奇古怪的东西甚至匪夷所思的操作的时候,我们编写的代码应当对这些预期的或者非预期的异常情况进行处理,使其能够回报错误信息,同时不至于崩溃。
易读易维护
“我生平最讨厌的只有两件事:别人的代码不写注释、我写代码要写注释”。代码写完之后不仅仅只是拿来运行的,也是拿来看的。好的、规范的注释可以为他人(以及未来的自己)提供快速理解代码功能的可能。提高团队协作的效率。
当然代码本身的结构也要在保证功能的同时尽量做到简洁清晰,不需要过多奇技淫巧。过多高级功能换来的不一定是性能的飞跃,也有可能是维护的昏天黑地。
代码保持格式化
代码保持格式化能够让人阅读起来更加轻松,提高阅读效率,降低理解成本。
Go语言常用的代码自动格式化工具有两个:gofmt和goimports。都是Go语言官方出品的工具。
gofmt可以将我们所编写的代码自动进行格式化成官方推荐的规范格式。而goimports可以根据代码内容自动修改import部分的内容。
GoLand和安装了Go插件的Vscode应该都带有这两个工具且默认启用。
注释规范
前面提到了注释在我们编写高质量代码的过程中具有重要的作用。那么下面是一些对于注释应当达成的规范要求。
注释应该解释代码的作用
在公共符号的开头部分添加注释,解释该公共符号的作用。
公共符号:变量、常量、函数以及结构体等。
注释应该解释代码如何做的
- 对于并非非常简短或者明显的公共功能实现要加以注释。
- 公共功能课程中没有提及是什么,猜测是一些比较复杂的高级语法之类不太容易第一眼看明白的内容?
- 对于所有函数必须加以注释。
注释应该解释代码实现的原因
对于一些不明显的写法,需要在注释中写明这一部分为什么这样做(可能的外部原因),同时提供一些上下文信息,方便出现问题时的快速定位。
注释应该解释代码什么情况会出错
Go语言的一大特点就是错误处理的代码量非常显著,所以注释中需要对代码的限制条件进行必要的解释。当然其他语言开发时也必须进行相应的注释,只是Go语言在这里适合作为例子而已。
命名规范
对于变量
- 简洁胜于冗长
- 缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
- 例如使用ServeHTTP而不是ServeHttp
- 使用XMLHTTPRequest或者xmlHTTPRequest
- 变量距离其被使用的地方越远,则需要携带越多的上下文信息
- 全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义
对于函数
- 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的
- 函数名尽量简短
- 当名为foo的包某个函数返回类型Foo时,可以省略类型信息而不导致歧义
- 当名为foo的包某个函数返回类型T时(T并不是Foo),可以在函数名中加入类型信息
对于包名
- 只由小写字母组成。不包含大写字母和下划线等字符
- 简短并包含一定的上下文信息。例如schema、task 等
- 不要与标准库同名。例如不要使用sync或者strings
以下规则尽量满足,以Go语言的标准库包名为例
- 不使用常用变量名作为包名。例如使用bufio而不是buf
- 使用单数而不是复数。例如使用encoding 而不是encodings
- 谨慎地使用缩写。例如使用fmt在不破坏上下文的情况下比 format 更加简短
流程规范
避免嵌套,保持正常流程清晰
对于程序员来说,最典型的屎山代码结构就是各种复杂的嵌套语句套来套去,时不时跳出一下跳到十万八千里外可能另外一个文件中。
因此要实现规范的编码流程,应当在开发时就有意的去避免过于复杂的嵌套逻辑的实现。必要时抽象解耦分层处理。这样也可以减少一段代码中过多的变量堆积,在阅读时挤占脑容量容易逻辑不清的问题。
尽量保持正常代码路径为最小缩进
优先处理错误情况/特殊情况,而将主代码逻辑放在特判的外面,减少主要逻辑的缩进级别。
函数的执行尽量顺着屏幕向下直线行进,便于提升代码的可读性和可维护性。
错误、异常处理
简单错误
对于仅仅出现一次,且不需要在其他地方捕获并处理的错误,称为简单错误。针对简单错误,我们可以:
- 优先使用
errors.New()方法来直接表示出来简单错误。 - 如果对于错误有格式化的需求,可以使用
fmt.Errorf()
错误链
Go语言提供了两个方法error.Wrap()和error.Unwrap()来用于将错误嵌套成链式结构,前者用于封装错误链,后者用于解封链提取错误。
在fmt.Errorf中也可以使用%w关键字来将错误关联到错误链中。
错误判定
- 判定一个错误是否为特定错误,使用
error.Is() - 该方法不同于使用
==直接判断,这样可以追踪到错误链上是否含有某个特定的错误。 - 如果需要获取到特定某种错误,可以使用
error.As()
panic与recover
panic与recover方法一般需要成对出现。在出现异常的时候使用panic方法调用特定的错误处理代码,在这段代码中需要有recover方法提供错误恢复。recover方法需要在defer函数中使用。
正因这种成对性,在没有recover的情况下单独使用panic会造成程序崩溃。所以一般情况下非必要可以直接使用error代替panic,可以一定程度上降低错误处理的复杂度。
小结
通过高质量编程规范课程的学习,以及时隔几天后这篇笔记的再次梳理,我对包括Go语言在内其实是普适的编程规范有了更深的了解。在今后的编码中,尤其是团队协作开发任务中也会尽量尝试去遵守、实践学到的内容。