这是我参与「第三届青训营 -后端场」笔记创作活动的的第5篇笔记。
一、 简介
1.1 什么是高质量
编写的代码能够达到正确可靠、简洁清晰的目标可称之为高质量代码。
- 正确性:是否考虑各种边界条件,错误的调用是否能够处理。
- 可靠性:异常情况或者错误的处理策略是否明确,依赖的服务出现异常是否能够正常处理。
- 简洁:逻辑是否简单,后续调整功能或者新增功能是否能够快速支持。
- 清晰:其他人在阅读理解代码的适合是否能清晰明白,重构或者修改功能是否不会担心出现无法预料的问题。
1.2 编程原则:
实际应用场景千变万化,各种语言的特性和语法各不相同,但是高质量编程遵循的原则是相通的。
- 简单性:消除“多余的复杂性”,以简单清晰的逻辑编写代码。在实际工程项目中,复杂的程序逻辑会让人害怕重构和优化,因为无法明确预知调整造成的影响范围。难以理解的逻辑,排查问题时也难以定位,不知道如何修复。
- 可读性:可读性很重要,因为代码是写给人看的,而不是机器。在项目不断迭代过程中,大部分工作是对已有功能的完善和扩展,很少完全下线某个功能,对应的功能代码实际会生存很长时间。已上线的代码在其生命周期内会被不同的人阅读几十上百次,难以理解的代码会占用后续每一个程序员的时间。
- 生产力:编程在当前更多的是团队合作,因此团队整体的工作效率是非常重要的一方面,为了降低新员工上手项目代码的成本,Go语言甚至通过工具强制统一所有代码格式。
二、 编码规范
2.1 编码规范 - 代码格式
推荐使用gofmt自动格式化代码
- gofmt:Go语言官方提供的工具,能自动格式化Go语言代码为官方统一风格。常见的IDE都支持方便的配置。
- goimports:也是Go语言官方提供的工具,实际等于gofmt加上依赖包管理。自动增删依赖的包引用、将依赖包按字母排序并分类。
2.2 编码规范 - 注释
注释应该做的:
- 注释应该解释代码作用。这种注释适合说明公共符号,比如对外提供的函数注释描述它的功能和用途。只有有在函数的功能简单而明显时才能省略这些注释,例如简单的取值和设置函数。另外注释要避免啰嗦,不要对显而易见的内容(通过名称可以很容易的知道作用)进行说明。
- 注释应该解释代码如何做的。对代码中复杂的,并不明显的逻辑进行说明,适合注释实现过程。
- 注释应该解释代码实现的原因。注释可以解释代码的外部因素,这些因素脱离了上下文后通常很难理解。
- 注释应该解释代码什么情况会出错。注释应该提醒使用者一些潜在的限制条件或者无法处理的情况。例如函数的注释中可以说明是否存在性能隐患,输入的限制条件,可能存在哪些错误情况,让使用者无需了解实现细节。
- 公共符号始终要注释。包中声明的每个公共符号:变量、常量、函数以及结构都需要添加注释;任何既不明显也不简短的公共功能必须予以注释;无论长度或复杂程度如何,对库中的任何函数都必须进行注释。但是不需要注释实现接口的方法。
2.3 编码规范 - 命名规范
- variable:
- 简洁胜于冗长。
- 缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写。
- 例如使用ServeHTTP而不是ServeHttp
- 使用XMLHTTPRequest或者xmlHTTPRequest
- 变量距离其被使用的地方越远,则需要携带越多的上下文信息
- 全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义
- function:
- 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的。
- 函数名尽量简短。
- 当名为foo的包某个函数返回类型为Foo时,可以省略类型信息而不导致歧义。
- 当名为foo的包某个函数返回类型T时(T并不是Foo),可以在函数名中加入类型信息。
- 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语言的高质量变成,但是别的语言也是类似的。