进行代码一致性控制


如果您认为一致性是质量代码的重要组成部分,那么本文适合您。


等着你:


  • 在Go中执行相同操作的不同方法(等效操作)
  • 影响代码统一性的不太明显的因素
  • 增强项目一致性的方法

一致性是我们的事情


首先,我们将确定所谓的“一致性”。


该程序的源代码看起来像是由一个人编写的,它们越一致。


值得注意的是,即使是同一个人,也经常会随着时间的流逝而改变自己的喜好,但是在涉及大量开发人员的大型项目中,一致性问题确实很严重。


有时,使用“一致性”代替“一致性”一词。 在本文中,我有时会使用上下文同义词来避免频繁的重言式。

有不同级别的一致性,例如,我们可以区分出三个最明显的级别:


  • 单一源文件的一致性
  • 包(或库)级别的一致性
  • 整个项目级别的一致性(如果由一个供应商控制)

列表越低,保持一致性越困难。 在这种情况下,在一个源代码文件级别上缺乏一致性似乎是最令人讨厌的。


您也可以从文件下移到函数或单个语句的级别,但是就我们而言,这已经是太多细节了。 在本文的结尾,将清楚为什么。


Go中的等效操作


Go并没有那么多具有相同拼写(语法差异)的相同操作,但是仍然存在意见分歧的空间。 一些开发人员喜欢选项A ,而第二个则喜欢B 这两个选项均有效且有其支持者。 允许使用任何形式的操作,这不是错误,但是使用不止一种形式的操作可能会损害代码的一致性。


您如何看待,大多数Go程序员使用这两种方法来创建长度为100的切片?


 // (A) new([100]T)[:] // (B) (&[100]T{})[:] 

答案


没有一个选项是首选。 在实际代码中,我从未见过其中任何一个的用法。


并在这种情况下使用make([]T, 100)




单次导入


有两种导入单个程序包的方法:


 // (A)   import "github.com/go-lintpack/lintpack" // (B)   import ( "github.com/go-lintpack/lintpack" ) 

同时, gofmtgoimports都不执行从一种形式到另一种形式的转换。 在您的项目中最有可能遇到这两种选择。


将指针标记为空值


只要Go具有内置函数new和替代的方法来获取指向新对象的指针,您将同时遇到new(T)&T{}


 // (A)   new new(T) new([]T) // (B)     &T{} &[]T{} 

创建一个空切片


至少有两种流行的方法来创建一个空切片(不要与nil切片混淆):


 // (A)   make make([]T, 0) // (A)   []T{} 

创建一个空的哈希表


您可能会发现创建空切片和map的分离不是很合逻辑,但是并非所有喜欢[]T{}都会使用map[K]V{}而不是make(map[K]V) 。 因此,这里的区别至少不过度。


 // (A)   make make(map[K]V) // (B)  - map[K]V{} 

十六进制文字


 // (A) af,   0xff // (B) AF,   0xFF 

用大小写混合写0xFf类型与一致性无关。 这应该由静态分析仪(线性)找到。 哪一个 例如,尝试gocritic


范围检查


在数学(和某些编程语言,但不是Go语言)中,您可以将范围描述为low < x < high 。 表达这种限制的源代码不能这样写。 同时,至少有两种流行的方法可以检查范围内的条目:


 // (A)     x > low && x < high // (B)    low < x && x < high 

运算符与非


您是否知道Go具有&^二进制运算符? 它称为and-not ,它执行与&相同的操作, &从右(第二)操作数应用于结果^


 // (A)   &^ ( ) x &^ y // (B)  &  ^ ( ) x & ^y 

我进行了一项调查,以确保通常会有不同的偏好。 最后,如果赞成&^的选择是一致的,那么这将必须是一遍检查,而不是代码一致性方面的选择。 令我惊讶的是,两种形式都发现了支持者。


实数文字


编写材料文字的方法有很多,但是即使在单个函数内,也可能破坏一致性的最常见功能之一是整体和材料部分(缩短或完整)的书写风格。


 // (A)      0.0 1.0 // (B)      ( ) .0 1. 

标签还是标签?


不幸的是,没有命名标签的约定。 剩下的就是选择一种可能的方法并坚持下去。


 // (A)     LABEL_NAME: goto PLEASE // (B) upper camel case LabelName: goto TryTo // (C) lower camel case labelName: goto beConsistent 

也可以使用Snake_case,但除了Go汇编程序之外,我没有在其他地方看到过这样的标签。 最有可能的是,您不应该坚持使用此选项。


无类型数字文字的类型说明


 // (A)     "=" var x int32 = 10 const y float32 = 1.6 // (B)     "=" var x = int32(10) const y = float32(1.6) 

执行被调用函数的右括号


在适合一行代码的简单调用的情况下,括号不会有问题。 例如,当出于某种原因一个函数或方法调用分布在多行上时,就会出现几个自由度,例如,您需要确定将括号放在自变量列表的什么位置。


 // (A)         multiLineCall( a, b, c) // (B)       multiLineCall( a, b, c, ) 

检查非零长度


 // (A)  "    0" len(xs) != 0 // (B)  " 0 " len(xs) > 0 // (C)  "  1 " len(xs) >= 1 

对于字符串,通常使用s != ""s == ""source )。


交换机中默认标签的位置


有两个合理的选择:将default一个或最后一个标签。 其他选项,例如“中间某处”-这是短绒棉布的工作。 从gocritic检查defaultCaseOrder gocritic发现它有助于获得一个更加惯用的版本,而go-consistent提供两个可能的选项之一,以使代码更加统一。


 // (A) default   switch { default: return "?" case x > 10: return "more than 10" } // (B) default   switch { case x > 10: return "more than 10" default: return "?" } 

一致


我们在上面列出了等效操作的列表。


如何确定要使用哪一个? 最简单的答案:在所考虑的部分项目中(特别是在整个项目中)使用频率较高的答案。


go-consistent程序分析指定的文件和程序包,计算一个或另一种选择的使用量,并提供在项目分析部分中用惯用的格式(频率最高的格式)替换频率较低的格式。


简单计数


此刻,每次出现的权重等于1。 有时这会导致一个事实,即一个文件决定了整个程序包的样式,这仅是因为该操作在其中更常用。 对于罕见的操作(例如创建一个空的map ,这尤其值得注意。


目前尚不清楚这是一种最佳策略。 该算法的这一部分将不难确定,也不会允许用户选择几个建议的算法之一。




如果$(go env GOPATH)/bin在系统PATH ,则以下命令将设置go-consistent


 go get -v github.com/Quasilyte/go-consistent go-consistent --help #    

回到一致性边界,这里是检查每个边界的方法:


  • 您可以通过在该文件上运行go-consistent来检查一个文件中go-consistent
  • 程序包中的一致性是在启动时使用单个程序包参数(或该程序包中的所有文件)计算的
  • 计算全局一致性将需要传递所有数据包作为参数

go-consistent的设计方式使其即使对于庞大的存储库也能给出答案,因为一次性将所有软件包都下载到内存中非常困难(至少在没有大量RAM的个人计算机上)。


另一个重要功能是零配置。 在没有任何标志和配置文件的情况下运行go-consistent可以在99%的情况下运行。


警告 :在项目上首次运行可能会生成大量警告。 这并不意味着代码编写不当,仅在这样的微观级别上控制一致性是非常困难的,并且如果控制是完全手动执行的,则不会花费很多人力。

去名字检查


函数参数或局部变量的命名不一致会降低代码的一致性。


对于大多数Go程序员而言,很明显erro代表错误的名字比err不太成功。 s vs str呢?


使用go-consistent方法无法解决检查变量名称一致性的任务。 没有当地惯例的宣言是很难做到的。


go-namecheck定义此清单的格式并允许对其进行验证,从而使遵循项目中定义的实体命名标准更加容易。


例如,您可以指定对于字符串类型的函数参数,值得使用标识符s代替str


该规则表示如下:


 {"string": {"param": {"str": "s"}}} 

  • string是捕获感兴趣类型的正则表达式
  • param替换规则的范围(范围)。 可能有几个
  • "str": "s"表示从strs的替换。 可能有几个

您可以使用捕获多个标识符的正则表达式来代替一对一替换。 例如,这是一条规则,要求使用后缀RE替换*regexp.Regexp类型的变量的re前缀。 换句话说, reFile规则将要求使用fileRE而不是fileRE


 { "regexp\\.Regexp": { "local+global": {"^re[AZ]\\w*$": "use RE suffix instead of re prefix"} } } 

所有类型均被视为忽略指针。 任何级别的间接操作都将被删除,因此无需为指向类型和类型本身的指针定义单独的规则。


描述这两个规则的文件如下所示:


 { "string": { "param": { "str": "s", "strval": "s" }, }, "regexp\\.Regexp": { "local+global": {"^re[AZ]\\w*$": "use RE suffix instead of re prefix"} } } 

该项目应该以一个空文件开始。 然后,在某个特定时刻,在代码审查中,将请求重命名结构中的变量或字段。 一个自然的反应可能是以命名约定文件中可验证规则的形式加强这些以前的非正式要求。 下次您可以自动找到问题。


go-namecheck安装和使用方式与go-consistent go-namecheck ,除了以下事实:为了获得正确的结果,您无需对整套程序包和文件进行检查。


结论


上面讨论的功能并不是个别重要的,而是会影响总体的整体一致性。 我们在微观级别检查了代码的均匀性,这不依赖于体系结构或应用程序的其他功能,因为这些方面最容易以几乎为零的误报进行验证。


如果您喜欢上面描述中的go-consistentgo-namecheck ,请尝试在您的项目中运行它们。 反馈对我来说是一份真正宝贵的礼物。


重要提示 :如果您有任何想法或补充,请告诉我们!
有几种方法:


  • 写这篇文章的评论
  • 创建问题一致
  • 实施您的实用程序并让全世界了解它

警告 :在CI中添加go-consistent和/或go-namecheck可能会过于激进。 每月运行一次并随后更正所有不一致之处可能是一个更好的解决方案。

Source: https://habr.com/ru/post/zh-CN429354/


All Articles