干货|Golang Programming Style(下)

点击上方“中兴开发者社区”,关注我们

每天读一篇一线开发者原创好文干货|Golang Programming Style(下)

接上文  Golang Programming Style(上)


表达式和语句

【规则6-2-1】 对于布尔类型的变量,应直接进行真假判断


正例:

干货|Golang Programming Style(下)

反例:

干货|Golang Programming Style(下)

【规则6-2-2】 在条件判断语句中,当整型变量与0 比较时,不可模仿布尔变量的风格,应当将整型变量用“==”或“!=”直接与0比较。


正例:

干货|Golang Programming Style(下)

反例:

干货|Golang Programming Style(下)

【建议6-2-1】 循环嵌套次数不大于3。


【建议6-2-2】 if 语句的嵌套层数不要大于3。


说明: 适当调整和优化判断逻辑,能够有效地控制if语句的嵌套层次,这对于代码的走查、测试、变更维护都有很大的帮助。如果能减少大语句块的嵌套深度,对于减轻代码阅读时的理解负担很有好处。


条件式通常有两种呈现形式:第一种形式是所有分支都属于正常行为;第二种形式则是条件式提供的答案只有一种是正常行为,其他都是不常见的情况。


这两类条件式有不同的用途,这一点应该通过代码表现出来。如果两条分支都是正常行为,就应该使用形如if-else的条件式;如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回,这样的单独检查常常被称为卫语句。


使用卫语句,能够有效的减少if语句嵌套层数。


【建议6-2-3】 使用for循环时,优先使用range 关键字而不是显式下标递增控制。


正例:

干货|Golang Programming Style(下)

反例:

干货|Golang Programming Style(下)

【建议6-2-4】 对于 range 的返回值,如果只需要第二项,则把第一项置为下划线。


正例:

干货|Golang Programming Style(下)


函数设计

【规则7-1-1】 函数命名要短小精悍和名副其实,避免误导。一般以它" 做什么" 来命名,而不是以它" 怎么做" 来命名。


说明: 函数命名名副其实就是指通过只读函数的名称就可以知道函数的功能,而不需要注释来补充。


给函数命名的方法:通过对要完成的功能进行分解和抽象,将功能分解成一个个单一的短小的功能实现体,对实现体的功能采用一个恰当的描述性名称命名,形成函数名称。


【规则7-1-2】 函数要短小,还要更短小。尽量控制在20行代码之内,包括空行和{}。


说明: 有几个原因造成我喜欢短而命名良好的函数。首先,如果每个函数的粒度都很小,那么函数被复用的机会就更大;其次,如果函数都是细粒度,那么函数在修改时也会更容易些;再次,高层函数调用命名良好的短小函数,使高层函数读起来就像一系列解释。


一个函数多长才算合适?长度不是问题,关键在于函数名称和函数本体之间的语义距离。建议函数体的规模不能太大,20 行封顶最佳。


【规则7-1-3】 函数应该做一件事,做好这件事,只做这一件事。


说明: 判断一个函数是否只做了一件事,可以通过两种方法:


(1)函数只是做了该函数名下同一抽象层上的步骤,则函数只做了一件事;


(2)如果一个函数内部的实现还可以拆分出一个函数,则该函数违反只做一件事原则。


【规则7-1-4】 函数的缩进层次不应该超过3层。


【规则7-1-5】 分隔指令与询问,不要设置多功能函数。


说明: 函数要么做什么事,要么回答什么事,两者不可兼得。如某个函数既返回对象状态值,又修改对象状态值,则需要建立两个不同的函数,其中一个负责查询对象状态,另一个负责修改对象状态。


【规则7-1-6】 封装底层操作的函数,不要实现为成员函数。


说明: 对于普通函数,我们可以使用Golang Stub来打桩,简单方便。


【建议7-1-1】 为简单功能编写函数。


说明: 虽然为仅用一两行就可完成的功能去编函数好象没有必要,但使用函数可使功能明确化,增加程序可读性,亦可方便维护、测试。



参数


【规则7-2-1】 禁止定义多于3个参数的函数。


说明: 函数参数设置最理想的参数个数是零,其次是一,再次是二,最后是三。参数不易
对付,它们有太多的概念性。另外从测试的角度看,参数更叫人为难。


【规则7-2-2】 函数参数不能含有标识参数。


说明: 标识参数丑陋不堪,函数往往根据它的多个取值而做多件事情,这与函数只做一件事原则违背。如果参数只是用于赋值,那么就不是标识参数,所以是否标识参数不是今通过形参来界定,而是看函数的实现是否因为函数的入参而做了多件事情。


【规则7-2-3】当struct变量作为参数时,应传送struct的指针而不传送struct,并且不得修改struct中的元素,用作输出时除外。



说明: 一个函数被调用的时候,形参会被一个个压入被调函数的堆栈中,在函数调用结束以后再弹出。一个结构所包含的变量往往比较多,直接以一个结构为参数,压栈出栈的内容就会太多,不但占用堆栈空间,而且影响代码执行效率。


如果使用结构的指针作为参数,因为指针的长度是固定不变的,结构的大小就不会影响代码执行的效率,也不会过多地占用堆栈空间。


如果传递的参数类型是 map、slice 和 channel 等引用类型,则不用传递指针,修改引用类型变量的初始地址除外(比如 json.Unmarshal)。


【规则7-2-4】在API函数中对输入参数的正确性和有效性进行检查,在内部能保证的条件下其他函数不用再进行重复检查。


说明: 很多程序错误是由非法参数引起的,我们应该充分理解并正确处理来防止此类错误,特别是指针参数地址非法判断和数组下标参数的边界判断,但是我们没有必要在多个函数中重复检查。


【规则7-2-5】防止将函数的参数作为工作变量。


说明: 将函数的参数作为工作变量,有可能错误地改变入参的内容,所以很危险。对于必须要改变的出参,最好也先使用局部变量,最后再将该局部变量赋值给该出参。


【规则7-2-6】如果参数列表中若干个相邻的参数类型相同,则可以在参数列表中省略前面变量的类型声明。


正例:

干货|Golang Programming Style(下)


【规则7-2-7】当 channel 作为函数参数时,根据最小权限原则,使用单向 channel。


说明: 从设计的角度考虑,所有的代码应该都遵循“最小权限原则”。


正例:在函数Parse中ch不会被改写

干货|Golang Programming Style(下)


返回值

【规则7-3-1】 返回值的个数不要大于3。

错误和异常设计


【规则8-1-1】 错误值统一分组定义,而不是跟着感觉走。


说明: 很多人写代码时,到处return errors.New(value),而错误value在表达同一个含义时也可能形式不同,比如“记录不存在”的错误value可能为:


1、"record is not existed."


2、"record is not exist!"


3、"###record is not existed!!!"


这使得相同的错误value撒在一大片代码里,当上层函数要对特定错误value进行统一处理时,需要漫游所有下层代码,以保证错误value统一,不幸的是有时会有漏网之鱼,而且这种方式严重阻碍了错误value的重构。


于是,我们可以参考C/C++的错误码定义文件,在Golang的每个包中增加一个错误对象定义文件,对于共性的错误对象定义,则放在公共的目录中。



正例:


干货|Golang Programming Style(下)

干货|Golang Programming Style(下)

【规则8-1-2】 失败的原因只有一个时,不使用error。


正例:

干货|Golang Programming Style(下)

反例:

干货|Golang Programming Style(下)

【规则8-1-3】 没有失败原因时,不使用error。


说明: error在Golang中是如此的流行,以至于很多人设计函数时不管三七二十一都使用error,即使没有一个失败原因,而该函数的调用者无疑是无奈的。


正例:


函数设计:

干货|Golang Programming Style(下)

函数调用:

干货|Golang Programming Style(下)

反例:


函数设计:

干货|Golang Programming Style(下)

函数调用:

干货|Golang Programming Style(下)

【规则8-1-4】 error/bool应放在返回值类型列表的最后。


正例:

干货|Golang Programming Style(下)

【规则8-1-5】 错误逐层传递时,层层都加日志


【规则8-1-6】 错误处理巧用defer。


正例:

干货|Golang Programming Style(下)

反例:

干货|Golang Programming Style(下)

【规则8-1-7】 当尝试几次可以避免失败时,不要立即返回错误。


说明: 如果错误的发生是偶然性的,或由不可预知的问题导致。一个明智的选择是重新尝试失败的操作,有时第二次或第三次尝试时会成功。在重试时,我们需要限制重试的时间间隔或重试的次数,防止无限制的重试。比如我们平时上网时,尝试请求某个URL,有时第一次没有响应,当我们再次刷新时,就有了惊喜。


【规则8-1-8】 当上层函数不关心错误时,不返回error。


说明: 对于一些资源清理相关的函数(destroy/delete/clear),如果子函数出错,打印日志即可,而无需将错误进一步反馈到上层函数,因为一般情况下,上层函数是不关心执行结果的,或者即使关心也无能为力,于是我们建议将相关函数设计为不返回error。


异常设计


【规则8-2-1】 在程序开发阶段,坚持速错,让程序异常崩溃。


说明: 所谓速错简单来讲就是“让它挂”,只有挂了你才会第一时间知道错误。在早期开发以及任何发布阶段之前,最简单的同时也可能是最好的方法是调用panic函数来中断程序的执行以强制发生错误,使得该错误不会被忽略,因而能够被尽快修复。


【规则8-2-2】 在程序部署后,应恢复异常避免程序终止。


说明: 在Golang中,虽然有类似Erlang进程的Goroutine,但需要强调的是Erlang的挂,只是Erlang进程的异常退出,不会导致整个Erlang节点退出,所以它挂的影响层面比较低,而Goroutine如果panic了,并且没有recover,那么整个Golang进程(类似Erlang节点)就会异常退出。所以,一旦Golang程序部署后,在任何情况下发生的异常都不应该导致程序异常退出,我们在上层函数中加一个延迟执行的recover调用来达到这个目的,并且是否进行recover需要根据环境变量或配置文件来定,默认需要recover。


正例:

干货|Golang Programming Style(下)

注:有时需要在延迟函数中释放资源,比如该携程在异常前进行了read channel操作,由于异常的发生使得该携程没有完成write channel操作,这会使得该channel后续的操作阻塞,所以必须在延迟函数中根据标志位进行write channel操作,以便操作始终都是闭合的。


【规则8-2-3】 对于不应该出现的分支,使用异常处理。


说明: 当某些不应该发生的场景发生时,我们就应该调用panic函数来触发异常。


正例:

干货|Golang Programming Style(下)

【规则8-2-4】 针对入参不应该有问题的函数,使用异常设计。


说明: 入参不应该有问题一般指的是硬编码,而不是API的外部输入。当调用者明确知道输入不会引起函数错误时,要求调用者检查这个错误是不必要和累赘的。我们应该假设函数的输入一直合法,当调用者输入了不应该出现的输入时,就触发panic异常。


正例: 库函数MustCompile的实现


 整洁测试


【建议9-1】 测试用例中不应该存在复杂的循环和条件控制语句。


说明: 测试用例对可读性的要求非常高,如果出现大量的循环、条件控制语句,将大大地损害了用例的可读性。一般地,测试用例应该是由若干条陈述句所组成,越简单越好。


【建议9-2】 测试代码和生产代码一样重要。

说明: 测试代码不是二等公民,它需要被思考、被设计和被照料,它该像产品代码一般保持整洁。


【建议9-3】 整洁的测试有三个要素:可读性,可读性和可读性。


说明: 在测试代码中,可读性甚至比生产代码还重要。生产代码的正确性由测试代码来保证,而测试代码的正确性只能由自己的可读性来保证,让错误无处藏身。



【建议9-4】 测试应该是黑盒的。


说明: 避免根据代码编写测试。


【建议9-5】 不要为测试而在产品代码中创建特别的逻辑。


说明: 禁止为了使测试通过而在产品代码中增加条件分支等测试辅助代码,而是尽可能的通过测试重构来避免,以减少测试代码对产品代码的入侵。


扩展阅读

干货|GoMock框架使用指南

干货|GoStub框架二次开发实践

干货|GoConvey框架使用指南

干货丨Golang事务模型


干货|Golang Programming Style(下)