Golang代码优化实战 | 青训营笔记

87 阅读6分钟

这是我参与「第五届青训营 」笔记创作活动的第3天。今天上课的内容为如何提高代码质量,进行高质量编程,这篇笔记是课堂内容的一个简单记录。

通过之前的课程,大家已经对Go语言有了一些基础,也编写了应用程序,今天的课程主要聚焦于真正开发项目的时候,作为开发者需要注意的代码优化问题,如何写出更加简明易懂的代码,以及如何利用Go语言特性来优化代码性能,是这节课我们讨论的问题。

高质量代码的一些特性

问:什么是高质量?

答:编写的代码能够达到正确可靠、简洁清晰的目标可称之为高质量代码

如何编写高质量的代码,实际应用场景千变万化,各种语言的特性和语法各不相同,但是也是有一些通用原则。

  • 简单性

    消除“多余的复杂性”,以简单清晰的逻辑编写代码。 在实际工程项目中,复杂的程序逻辑会让人害怕重构和优化,因为无法明确预知调整造成的影响范围 难以理解的逻辑,排查问题时也难以定位,不知道如何修复

  • 可读性

    可读性很重要,因为代码是写给人看的,而不是机器。在项目不断迭代的过程中,大部分工作是对已有功能的完善或扩展,很少会完全下线某个功能,对应的功能代码实际会生存很长时间。已上线的代码在其生命周期内会被不同的人阅读几十上百次。

  • 生产力

    编程在当前更多是团队合作,因此团队整体的工作效率是非常重要的一方面。为了降低新成员上手项目代码的成本,G0语言甚至通过工具强制统一所有代码格式。

编码在整个项目开发链路中的一个节点,遵循规范,避免常见缺陷的代码能够降低后续联调、测试、验证、上线等各个节点的出现问题的概率,就算出现问题也能快速排查定位。

编码规范

代码格式

Go语言官方提供了工具 gofmt 来进行代码规范,使得团队代码可以保持统一风格,个人来讲比起其他没有统一风格语言,减少了很多因为调整风格而进行的代码提交,使用diff工具也会更加方便。

gofmt的配置十分便捷,在IDE中可自行配置,每次保存之后都会自动format,很方便。

代码注释

Good code has lots of comments,bad code requires lots of comments

好的代码有很多注释,坏代码需要很多注释

                                   ———— Dave Thomas and Andrew Hunt

虽然说,好的代码,本身就是注释,但如果在新接手一个项目时,好的注释可以快速帮你上手,下面来探讨下好的注释习惯。

  • 注释应该解释代码的作用,对外暴露的方法要加上注释,当然注释也不能太啰嗦,并且显而易见的方法就不需要注释了,函数名就是注释。 比如这个函数最好就不用加注释。
func IsTableFull() bool {
///
}
  • 第二种注释是对代码中复杂的,并不明显的逻辑进行说明,适合注释实现过程

image.png

上面这段代码是给新url加上最近的referer信息,并不是特别明显,所以注释说明了一下

下面的是一个反例,虽然是对过程注释,但是描述的是显而易见的流程,注意不要用自然语言直接翻译代码作为注释,信息冗余还好,有时候表述不一定和代码一致

image.png

  • 第三条,注释可以解释代码的外部因素,这些因素脱离上下文后通常很难理解

示例中有一行shouldRedirect=:false的语句,如果没有注释,无法清楚地明白为什么会设置false 所以注释里提到了这么做的原因,给出了上下文说明

image.png

  • 第四,注释应该提醒使用者一些潜在的限制条件或者会无法处理的情况

例如函数的注释中可以说明是否存在性能隐患,输入的限制条件,可能存在哪些错误情况,让使用者无需了解实现细节

image.png 示例介绍了解析时区字符串的流程,同时对可能遇到的不规范字符串处理进行了说明

命名规范

  • 变量命名

    变量命名要考虑名称的含义。函数提供给外部调用时,签名的信息很重要,要将自己的功能准确表现出来,自动提示一般也会提示函数的方法签名,通过参数名更好的理解功能很有必要,节省时间

  • 函数命名

    • 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的
    • 函数名尽量简短
    • 当名为foo的包某个函数返回类型Foo时,可以省略类型信息而不导致歧义
    • 当名为foo的包某个函数返回类型T时(T并不是Foo),可以在函数名中加入类型信息
  • 包命名

    • 只由小写字母组成。不包含大写字母和下划线等字符
    • 简短并包含一定的上下文信息。例如schema、task等
    • 不要与标准库同名。例如不要使用sync或者strings

    下规则尽量满足,以标准库包名称为例:

    • 不使用常用变量名作为包名。例如使用bufio而不是buf
    • 使用单数而不是复数。例如使用encoding而不是encodings
    • 谨慎地使用缩写。例如使用fmt在不破坏上下文的情况下比format更加简短

控制流程

  • 避免嵌套,保持正常流程清晰
    • 优先处理错误情况/特殊情况,尽量保证早返回或继续循环来减少嵌套
    //bad
    func OneFunc() error {
       err := doSomething()
       if err == nil {
           err := doAnother()
           if err == nil {
               return nil //normal case
           }
           return err
       }
       return err
    }
    

最常见的正常流程的路径被嵌套在两个f条件内,成功的退出条件是return nil,必须仔细匹配大括号来发现函数最后一行返回一个错误,需要追溯到匹配的左括号,才能了解何时会触发错误。 如果后续正常流程需要增加一步操作,调用新的函数,则又会增加一层嵌套。

调整之后:

//gode
func OneFunc() error {
    if err := doSomething(); err != nil {
        return err
    }
    if err := doAnother(); err != nil {
        return err
    }
    return nil
}

可以看到代码的阅读性提高了很多。