高质量编程 | 青训营笔记

95 阅读8分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第5篇笔记。

一、 简介

1.1 什么是高质量

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

  • 正确性:是否考虑各种边界条件,错误的调用是否能够处理。
  • 可靠性:异常情况或者错误的处理策略是否明确,依赖的服务出现异常是否能够正常处理。
  • 简洁:逻辑是否简单,后续调整功能或者新增功能是否能够快速支持。
  • 清晰:其他人在阅读理解代码的适合是否能清晰明白,重构或者修改功能是否不会担心出现无法预料的问题。

1.2 编程原则:

实际应用场景千变万化,各种语言的特性和语法各不相同,但是高质量编程遵循的原则是相通的。

  • 简单性:消除“多余的复杂性”,以简单清晰的逻辑编写代码。在实际工程项目中,复杂的程序逻辑会让人害怕重构和优化,因为无法明确预知调整造成的影响范围。难以理解的逻辑,排查问题时也难以定位,不知道如何修复。
  • 可读性:可读性很重要,因为代码是写给人看的,而不是机器。在项目不断迭代过程中,大部分工作是对已有功能的完善和扩展,很少完全下线某个功能,对应的功能代码实际会生存很长时间。已上线的代码在其生命周期内会被不同的人阅读几十上百次,难以理解的代码会占用后续每一个程序员的时间。
  • 生产力:编程在当前更多的是团队合作,因此团队整体的工作效率是非常重要的一方面,为了降低新员工上手项目代码的成本,Go语言甚至通过工具强制统一所有代码格式。

二、 编码规范

2.1 编码规范 - 代码格式

推荐使用gofmt自动格式化代码

  • gofmt:Go语言官方提供的工具,能自动格式化Go语言代码为官方统一风格。常见的IDE都支持方便的配置。
  • goimports:也是Go语言官方提供的工具,实际等于gofmt加上依赖包管理。自动增删依赖的包引用、将依赖包按字母排序并分类。

2.2 编码规范 - 注释

注释应该做的:

  1. 注释应该解释代码作用。这种注释适合说明公共符号,比如对外提供的函数注释描述它的功能和用途。只有有在函数的功能简单而明显时才能省略这些注释,例如简单的取值和设置函数。另外注释要避免啰嗦,不要对显而易见的内容(通过名称可以很容易的知道作用)进行说明。
  2. 注释应该解释代码如何做的。对代码中复杂的,并不明显的逻辑进行说明,适合注释实现过程。
  3. 注释应该解释代码实现的原因。注释可以解释代码的外部因素,这些因素脱离了上下文后通常很难理解。
  4. 注释应该解释代码什么情况会出错。注释应该提醒使用者一些潜在的限制条件或者无法处理的情况。例如函数的注释中可以说明是否存在性能隐患,输入的限制条件,可能存在哪些错误情况,让使用者无需了解实现细节。
  5. 公共符号始终要注释。包中声明的每个公共符号:变量、常量、函数以及结构都需要添加注释;任何既不明显也不简短的公共功能必须予以注释;无论长度或复杂程度如何,对库中的任何函数都必须进行注释。但是不需要注释实现接口的方法。

2.3 编码规范 - 命名规范

  1. variable:
  • 简洁胜于冗长。
  • 缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写。
    • 例如使用ServeHTTP而不是ServeHttp
    • 使用XMLHTTPRequest或者xmlHTTPRequest
  • 变量距离其被使用的地方越远,则需要携带越多的上下文信息
    • 全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义
  1. function:
  • 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的。
  • 函数名尽量简短。
  • 当名为foo的包某个函数返回类型为Foo时,可以省略类型信息而不导致歧义。
  • 当名为foo的包某个函数返回类型T时(T并不是Foo),可以在函数名中加入类型信息。
  1. package:
  • 只有小写字母组成。不包含大写字母和下划线等字符。
  • 简短并包含一定的上下文信息。例如:schema,task等。
  • 不要与标准库同名,例如不要使用sync或者strings。

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

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

2.4 编码规范 - 控制流程

  • 避免嵌套,保持正常流程清晰。比如if else中,如果两个分支都包含return语句,则可以去除冗余的else。
  • 尽量保持正常代码路劲为最小缩进。优先处理错误情况,尽早返回或继续循环来减少嵌套。

2.5 编码规范 - 错误和异常处理

简单错误

  • 简单错误指的是仅出现一次的错误,且在其他地方不需要捕获该错误。
  • 优先使用errors.New来创建匿名变量直接表示简单错误。
  • 如果有格式化的需求,使用fmt.Errorf

错误的Wrap和Unwrap

  • 错误的Wrap实际上是提供了一个error嵌套了另一个error的能力,从而生成了一个error的跟踪链。
  • 在fmt.Errorf中使用:%w关键字来将一个错误关联至错误链中。

错误判定

  • 判定一个错误是否为特定错误,使用errors.Is
  • 不同于使用==,使用该方法可以判定错误链上所有错误是否包含有特定的错误。
  • 在错误链上获取特定种类的错误,使用errors.As

panic

  • 不建议在业务代码中使用panic
  • 调用函数不包含recover会造成程序崩溃
  • 若问题可以被屏蔽或者解决,建议使用error代替panic。
  • 当程序启动阶段发生不可逆转的错误时,可以在init或main函数中使用panic。

recover

  • recover只能在被defer的函数中使用
  • 嵌套无法生效
  • 只在当前goroutine生效
  • defer的语句是后进先出

三、性能优化建议

3.1 性能优化建议 - slice

slice预分配内存,尽可能在使用make()初始化切片时提供容量信息。

  • 切片本质是一个数组片段的描述
    • 包括数组指针
    • 片段的长度
    • 片段的容量(不改变内存分配情况下的最大长度)
  • 切片操作并不复制切片指向的元素
  • 创建一个新的切片会复用原来切片的底层数组

了解了slice的基本结构之后,还有个问题需要注意。原切片由大量的元素构成,在原切片的基础上切片,虽然只使用了很小一段,但底层数组在内存中仍然占据了大量空间,得不到释放。可以使用copy代替re-slice。

3.2 性能优化建议 - Map

map预分配内存:

  • 不断向map中添加元素的操作会触发map的扩容。
  • 提前分配好空间可以减少内存拷贝和Rehash的消耗。
  • 建议根据实际需求提前预估好需要的空间。

3.3 性能优化建议 - 字符串处理

使用strings.Builder。使用+拼接性能最差,string.Builder,bytes.Buffer相近,strings.Buffer更快。

分析:

  • 字符串在Go语言中是不可变类型,占用内存大小是固定的。
  • 使用+每次都会重新分配内存。
  • strings.Builder,bytes.Buffer底层都是[]byte数组。
  • 内存扩容策略,不需要每次拼接重新分配内存。

3.4 性能优化建议 - 空结构体

使用空结构体节省内存

  • 空结构体struct{}实例不占据任何的内存空间。
  • 可以作为各种场景下的占位符使用。
    • 节省资源
    • 空结构体本身具备很强的语义,即这里不需要任何值,仅作为占位符。

3.5 性能优化建议 - atomic包

使用atomic包:

  • 锁的实现是通过操作系统实现,属于系统调用。
  • atomic操作是通过硬件来实现。
  • sync.Mutex应该用来保护一段逻辑,不仅仅用于保护一个变量。
  • 对于非数值操作,可以使用atomic.Value,能承载一个interface{}。

四、总结

在实际项目中,往往需要注意很多细节,才能高质量的编程,提高项目的性能。虽然学习的是Go语言的高质量变成,但是别的语言也是类似的。