这是我参与「第三届青训营 -后端场」笔记创作活动的的第四篇笔记。
高质量编程
1.1 编程原则
- 简单性 消除"多余的负责性",以简单清晰的逻辑编写代码
在实际工程项目中,复杂的程序逻辑会让人害怕重构和优化,因为无法明确预知调整造成的影响范围,并且难以理解的逻辑会使得排查问题时也难以定位,不知道如何修复
- 可读性
可读性很重要,因为代码是写给人看的,而不是机器。
在项目不断迭代的过程中,大部分工作是对已有功能的完善或扩展,很少会完全下线某个功能,对应的功能代码实际会生存很长
- 生产力 编程在当前更多是团队合作,因此团队整体的工作效率是非常重要的一方面
1.2 代码规范
1.2.1 代码格式
- gofmt
gofmt 作为代码格式工具是Go语言官方提供的工具,能够自动格式化Go语言代码为官方统一风格。
- goimports
goimports 也是Go语言官方提供的工具实际等于gofmt加上依赖包管理,它能够自动增删依赖的包应用,将依赖包按照字母序排序并分类
1.2.2 注释
在大多数时候我们都在关注代码实现,但是注释的重要性容易被忽略,从而引起一些项目开发的理解上的问题,因此每当我们实现一个函数的时候更需要添加注释来解释函数的含有,才能更加有利于团队项目开发。
对于注释我们应该遵循一下四点规则;
- 作用
首先是注释应该解释代码的作用,这种注释适合说明公共符号,比如对外提供的函数注释描述它的功能和用途,只有在函数的功能简单而明显式才能省略这些注释(例如简单的取值函数和设值函数)
其次注释要避免啰嗦,不对显而易见的内容进行说明例如以下代码:
func IsEmpty() bool
- 如何做的
对于比较复杂的代码,我们需要对一些逻辑并不清除的地方进行注解,对其解释实现的过程
- 实现的原因
注释可以解释代码的外部因素,这些因素脱离上下文后通常很难理解。
例如在一个函数中我们将一个开关的布尔值设置为false,当上下文关联性不强的时候,我们需要来解释为什么需要将其设置false状态。
- 什么情况会出错
最后注释应该提醒使用者一些潜在的限制条件或者会无法处理的轻快
例如函数的注释中可以说明是否存在性能隐患,输入的限制条件,可能存在那些错误情况,让使用者无需了解实现细节
1.2.3 命名规范
- 变量
-
简洁胜于冗长
-
缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写例如:
使用ServeHTTP而不是ServelHttp
使用XMLHTTPRequest或者xmlHTTPRequest
-
变量距离其被使用的地方越远,则需要携带越多的上下文信息,全局比哪里在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义
- 函数
-
函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的
-
函数名尽量简短
-
当名为foo的包某个函数返回类型为Foo时,可以省略类型信息而不导致歧义
-
当名为foo的包某个函数返回类型为T时(T 并不是Foo),可以在函数名中加入类型信息
- 包
- 只由小写字母组成,不包含大写字母和下划线等字符
- 简短并包含一定的上下文信息,例如 schema、task等
- 不要与标准库同名,例如不要使用 sync 或者 strings
- 需要用多个单词表达上下文的命名可以使用缩写,例如使用 strconv 而不是 stringconversion
以下规则尽量满足,以标准库包名为例
- 不使用常用变量名作为包名。例如使用 bufio 而不是 buf
- 使用单数而不是复数。例如使用 encoding 而不是 encodings
- 谨慎地使用缩写。例如使用 fmt 在不破坏上下文的情况下比 format 更加简短
包名也涉及到项目代码结构的划分和层次安排,具体名称不同项目会有细微差异,实际保持项目内风格统一。
1.2.4 控制流程
当我们能够给变量或者函数一个合适的名称过后,我们接下来就要开始实现具体的流程功能了,经常使用的就是if else语句了,我们在使用这种条件控制语句又有其他的规范条件。
首先在使用条件控制语句的时候,我们需要避免嵌套,从最简单的一个if else条件开始,如果两个分支都包含return语句,则可以去除冗余的else语句方便后续维护。例如以下代码
if foo{ if foo{
return a; return a;
}else{ ----修改为---> }
return b; return b;
}
1.2.5 错误
- 简单错误 简单错误指的是仅仅出现一次的错误,且在其他地方不需要捕获该错误,我们通常优先使用errors.New来创建匿名变量来直接表示简单错误。如果有格式化的要求,我们可以通过fmt.Errorf进行对其格式化。例如以下代码
func defaultCheckRedirect(req *Request,via []*Request) errer{
if len(via) >= 10{
return error.New("stopped after 10 redirects")
}
return nil
}
- 复杂错误 对于复杂的错误,有时候并不能简单描述,因此我们利用错误的包装提供了一个error嵌套另一个error的能力,生成一个error的跟踪链,同时结合错误的判定方法来确认调用链中是否有关注的错误出现。并且我们可以通过fmt.Errorf中使用:%w关键字来将一个错误wrap至错误链中。
list,_ err := c.GetBytes(cache.Subkey(a.actionID,"srcfiles"))
if err != nil{
return fmt.Errorf("reading secfiles list: %w",err)
}
- 错误判定
- 判定一个错误是是否为特定错误,使用 errors.Is
不同于使用 == ,使用该方法可以判定错误链上的所有错误是否含有特定的错误
```
data, err = lockedfile.Read(targ)
if errors.Is(err, fs.ErrNotExist) {
// Treat non-existent as empty, to bootstrap the "latest" file
// the first time we connect to a given database
return []byte{}, nil
}
return data, err
```
2. 在错误链上获取特定种类的错误,使用 errors.As
errors.As 和 errors.Is 的区别在于 As 会提取出调用链中指定类型的错误,并将错误赋值给定义好的变量,方便后续处理
示例中是把问题的 path 打印出来了
```
if _, err := os.Open("non-existing"); err != nil {
var pathError *fs.PathError
if errors.As(err, &pathError) {
fmt.Println("Failed at path:", pathError.Path)
} else {
fmt.Println(err)
}
}
```