这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天
本次跟着张雷老师学习高质量编程中涉及到的比较重要的编码规范。
1.什么是高质量代码
定义:编写的代码能够达到正确可靠、简洁清晰的目标可称之为高质量代码。
高质量代码满足的条件:
- 各种边界条件考虑完备
- 异常条件处理,稳定性保证
- 易读易维护
2.高质量代码的注意事项
本次我们将从代码格式、注释、命名规范、控制流程、错误和异常处理等方面来展开描述,通过理论与实例的结合,来展示高质量代码的一些必要条件。
2.1 注释:
在google官方编码规范指南里面,也有说到关于公共符号注释的事项。
对于公共符号的注释,我们要遵守两条规则:
- 任何既不明显也不简短的公共功能必须予以注释
- 无论长度或复杂程度如何,对库中的任何函数都必须进行注释
有一个例外是:不需要注释实现接口的方法(应该是因为接口在声明的时候已经有注释了,此时实现接口再注释便显得冗余)
对于注释,我们要注意:
- 注释需要提供额外的信息,而不是对已经有的东西进行描述
- 当函数名足够清楚的时候,不需要重复描述(比如函数 IsTableFull)
- 注释应该结束代码如何做的,比如对于复杂的代码适当地注释实现过程(但对于for range循环这种,解释对于每一个元素进行处理,则没有必要)
- 注释应该解释代码实现的原因,比如说代码进行了简化实现/未对所有功能进行实现,就需要提供额外上下文
- 注释应该解释代码的限制条件(课程中说到,如果函数过于复杂,这样的注释有助于用户在不明白函数具体实现的情况下来使用函数,当然这个注释就是针对于整个函数的)
小结:
- 代码是最好的注释
- 注释应该提供代码未表达出的上下文信息(比如说为什么做,还有代码限制条件等)
缩进有点问题的样子
2.2 代码格式
代码格式上,推荐使用gofmt自动格式化代码。或者使用go imports,go imports也是go官方提供的工具,实际上相当于gofmt加上依赖包管理。
2.3 命名规范
命名规范的注意事项:
2.3.1 变量命名
-
简洁胜于冗长
-
缩略词全部大写,但当其位于变量开头且不需要导出时,使用全小写
- 例如使用ServeHTTP 而不是 ServeHttp(看到没有,这里HTTP是缩略词,所以这里全大写)
- 使用XMLHTTPRequest或者 xmlHTTPRequest(如果在开头则可以小写)
-
变量距离其被使用的地方越远,则需要携带越多的上下文信息
- 全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义(全局变量作用范围比较大嘛,所以需要有更多信息)
-
循环中使用的例子
课上举的例子是for循环中的下标设置
//bad for index := 0; index < len(s); index++{ //... } //good for i := 0; i < len(s); i++{ //... }说的是i和index的作用域都仅限于for循环内部,index的额外冗长几乎没有增加对程序的理解(不过我觉得如果中间循环对i的使用比较复杂,那还是用index比较好,毕竟代码多了还是容易花的)
2.3.2 函数命名
函数参数上,则需要增加变量名中的信息量。在函数命名上,注意:
- 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的(有道理,因为使用函数要引入对应的包嘛)
- 函数名尽量简短
- 当名为foo的包 某个函数返回类型Foo时,可以省略类型信息而不导致歧义(就是直接New的意思吧,实际上我感觉和第一条是类似的,其实就是返回类型被包名携带了)
- 当名为foo的包 某个函数返回类型T时(T并不是foo),可以在函数名中加入类型信息(即NewXXX)
后面还给了http的例子,这个还是挺有意思的,即http包中的函数,用Serve命名就可以,不需要使用ServeHTTP这样子。
2.3.3 包命名
在包的命名规范上,要满足:
- 只由小写字母组成,不包含大写字母和下划线等字符(注意是全小写)
- 简短并包含一定的上下文信息,例如schema、task等
- 不要与标准库同名。例如不要使用sync或strings
以下规则尽量满足(不是都要满足),以标准库包名为例:
- 不使用常用变量作为包名。例如使用bufio而不是buf(而且感觉bufio更详细吧,多了io了,而且buf是常用变量)
- 使用单数而不是复数。例如使用encoding而不是encodings(这个有什么说法么?就是简短?不过看来上面已经有一个strings包了,不过string本身是关键字吧,应该也是无奈之举)
- 谨慎使用缩写,例如使用fmt在不破坏上下文的情况下比format更加简短
2.3.4 小结
命名规范小结:
- 核心目标是降低阅读理解代码的成本
- 重点考虑上下文信息,设计简洁清晰的名称
很有意思的一个说法:
Good naming is like a good joke. If you have to explain it, it's not funny. -- Dave Cheney
好的命名就像一个好笑话。如果你必须解释它,那就不好笑了。
2.4 控制流程
控制流程应该就是分支和条件判断这些吧,注意事项有:
-
避免嵌套,保证正常流程清晰(我想到那个波动拳了)
当时波动拳的处理方法应该是先处理错误的然后return掉,这样就可以避免分支嵌套
//Bad if foo{ return x } else { return nil } //Good if foo{ return x } return nil不过其实对于这种简化的场景,我因为格式问题会觉得上面的更好看,但实际上一旦场景复杂起来,上面的就不好了,想一下如果后面还需要在分支判断中添加别的东西,而且这些包含了其他的分支判断,使用上面的就会在else语句中进行进一步嵌套,这显然是不好的(在这种情况下,当前的处理就像是对于波动拳的处理,把错误要提早返回的单独来出来,然后剩下的主干逻辑继续处理剩下的东西)
-
尽量保持正常代码路径为最小缩进
解读一下“尽量保持正常代码为最小缩进”这句话,感觉还是和波动拳的处理类似,就是上面说过:“优先处理错误情况/特殊情况,尽早返回或继续循环来减少嵌套”。
个人理解上,我觉得是因为:正常代码占比肯定比其他的多,所以把正常代码为最小缩进(没有在其他分支里面)是可读性最好的。优先处理错误和特殊情况,就是为了尽早返回,这样剩下的内容就都是正常代码了,那么就不需要进行缩进和分支的嵌套。
下面是优化前后的代码: 优化前:
//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 }这里优化的做法更加贴近golang语法,我原来的想法还是基于C++的,就是类似:
xx = xxxxxx() if xx == nil{ return xxx }不过显然golang语法中的实现会更简洁一些
控制流程规范小结
- 线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支
- 正常流程代码沿着屏幕向下移动(是在说有goto的吗?还是说函数的定义顺序上?)
- 提升代码可维护性和可读性
- 故障问题大多出现在复杂的条件语句和循环语句中
2.5 错误和异常处理
2.5.1 简单错误
- 简单的错误指的是仅出现一次的错误,且在其他地方不需要捕获该错误(所以我们只需要告诉错误的原因就可以了)
- 优先使用 errors.New 来创建匿名变量来直接表示简单错误(这样就可以描述错误原因了)
- 如果有格式化的要求,使用fmt.Errorf
2.5.2 错误的wrap和unwrap
注意这里的错误是名词,而不是形容词,指的是对于错误error的wrap和unwrap操作
- 错误的wrap实际上是提供了一个error嵌套另一个error的能力,从而生成了一个error的跟踪链
- 在fmt.Errorf中使用%w关键字来将一个错误关联至错误链中
引入了wrap和unwrap之后,就有了错误链。而既然有了错误链,那么这里对于错误之间的比较就不能使用等号了。
- 判定一个错误是否为特定错误,使用errors.Is
- 不同于使用==,使用该方法可以判定错误链上的所有错误是否含有特定的错误(直接使用==是判断不出来的)
然后还有一个errors.As方法,其和errors.Is方法的区别就在于,这种方法能够从错误链中取出中间一个具体错误
2.5.3 异常部分
panic:
- 不建议在业务代码中使用panic
- 调用函数不包含recover会造成程序崩溃
- 若问题可以被屏蔽或解决,建议使用error代替panic
- 当程序启动阶段发生不可逆转的错误时,可以在init或main函数中使用panic(启动阶段这种错误需要及早暴露出来,所以使用panic,因为启动都出问题了启动起来了也没有意义,所以。。)
recover
- recover只能在被defer的函数中使用
- 嵌套无法生效
- 只在当前go routinue中生效(意思是那个调用栈?)
- defer的语句是后进先出
2.5.4 小结
- error尽可能提供简明的上下文信息链,方便定位问题(应该就是为了定义本次问题在哪里出现吧,所以上下文信息需要给够)
- panic用于真正异常的情况
- recover生效范围:在当前go routine的被defer函数中生效