高质量编程-编码规范 | 青训营笔记

181 阅读7分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 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函数中生效