使用go-critic解析器为Go贡献力量


您可能还记得最近发布的Go的新静态分析器,称为go-critic


我用它检查了golang / go项目,并发送了一些补丁来修复那里发现的一些问题。


在本文中,我们将分析更正后的代码,并且还将激励我们将更多此类更改发送给Go。


最不耐烦的: 奖杯更新列表



dupSubExpr


我们都会犯错,而且常常是疏忽大意。 Go语言是一种您有时不得不编写无聊的代码和样板代码的语言,有时会导致拼写错误和/或复制/粘贴错误。


CL122776包含针对dupSubExpr发现的错误的修复:


 func (a partsByVarOffset) Less(i, j int) bool { - return varOffset(a.slots[a.slotIDs[i]]) < varOffset(a.slots[a.slotIDs[i]]) + return varOffset(a.slots[a.slotIDs[i]]) < varOffset(a.slots[a.slotIDs[j]]) // ^__________________________________^ } 

注意左右的索引。 在更正之前,运算符<的LHS和RHS相同,并且dupSubExpr工作。


commentedOutCode


如果您的项目由版本控制系统赞助,那么与其将其包装在注释中来禁用代码,不如将其完全删除是值得的。 有例外,但更多时候,这种“死”代码会干扰,混淆并可能隐藏错误。


commentedOutCode可以找到这样一个有趣的片段( CL122896 ):


 switch arch.Family { // ...  case clause. case sys.I386: return elf.R_386(nr).String() case sys.MIPS, sys.MIPS64: // return elf.R_MIPS(nr).String() // <- 1 case sys.PPC64: // return elf.R_PPC64(nr).String() // <- 2 case sys.S390X: // return elf.R_390(nr).String() // <- 3 default: panic("unreachable") } 

还有一个更高的评论:


 // We didn't have some relocation types at Go1.4. // Uncomment code when we include those in bootstrap code. 

如果切换到go1.4分支并从注释中删除了这3行,则代码将无法编译,但是,如果在向导中取消注释,则一切正常。


通常,隐藏在注释中的代码需要删除或反向激活。


有时,在代码中访问过去的回声非常有用。


关于检测的困难

这是我最喜欢的支票之一,但却是最“嘈杂”的支票之一。


对于在编译器内部使用math/big软件包,存在很多误报。 在第一种情况下,这些通常是对所执行操作的解释性注释,在第二种情况下,它们是描述AST片段的代码的描述。 将这样的注释与真实的“死”代码区分开而不引入假否定是不平凡的。


这引起了这样一个想法:如果我们同意以某种方式专门化注释内的代码,那该怎么办? 然后将简化静态分析。 这可以是任何琐碎的事情,可以使定义这样的解释性注释变得容易,也可以使其变为无效的Go代码(例如,如果在行的开头添加井号,则# )。


另一类是带有显式TODO注释。 如果删除了该代码以进行注释,但是有明确说明为什么这样做以及计划修复此代码的时间,则最好不要发出警告。 这已经实现,但是可以更可靠地工作。


boolExprSimplify


有时人们会写奇怪的代码。 也许在我看来,但是逻辑( 布尔 )表达式有时看起来特别奇怪。


Go拥有出色的x86汇编程序后端(在这里落泪了),但是ARM确实做错了:


 if !(o1 != 0) { break } 

“如果不是o1不等于0” ...双重否定是经典。 如果您喜欢它,我邀请您熟悉CL123377 。 在那里您可以看到更正的版本。


更正的选项(适用于那些无法引诱进行审查的人)
 - if !(o1 != 0) { + if o1 == 0 { 

boolExprSimplify旨在简化以提高可读性(如果没有它,Go优化器将可以解决性能问题)。


不足


如果使用早期版本的Go,您会记得强制分号,缺少指针的自动解引用以及其他新功能几乎无法在今天看到的功能。


在旧代码中,您仍然可以看到以下内容:


 // -    : buf := (*bufp).ptr() // ...     : buf := bufp.ptr() 

CL122895中修复了几个不足的分析仪触发器。


appendCombine


您可能知道append可以将几个参数作为元素添加到目标切片中。 在某些情况下,这可以使您稍微提高代码的可读性,但是,这可能更有趣,它还可以加快程序的速度,因为编译器不会抑制兼容的append调用( cmd / compile:combinate append调用 )。


在Go中, appendCombine检查发现以下部分:


 - for i := len(ip) - 1; i >= 0; i-- { - v := ip[i] - buf = append(buf, hexDigit[v&0xF]) - buf = append(buf, '.') - buf = append(buf, hexDigit[v>>4]) - buf = append(buf, '.') - } + for i := len(ip) - 1; i >= 0; i-- { + v := ip[i] + buf = append(buf, hexDigit[v&0xF], + '.', + hexDigit[v>>4], + '.') + } 

 name old time/op new time/op delta ReverseAddress-8 4.10µs ± 3% 3.94µs ± 1% -3.81% (p=0.000 n=10+9) 

CL117615中的详细信息。


rangeValCopy


复制在range循环中迭代的值并不是什么秘密。 对于小于64个字节的小对象,您甚至可能不会注意到这一点。 但是,如果这样的循环位于“热”路径上,或者您迭代遍历了许多元素,那么开销可能是明显的。


Go具有相当慢的链接器 (cmd / link),并且在其体系结构中没有进行重大更改的情况下,就无法实现强劲的性能提升。 但是,您可以借助微优化稍微降低其效率。 每百分之一或两个计数。


rangeValCopy检查发现了几个周期,一次不需要复制数据。 这是其中最有趣的:


 - for _, r := range exports.R { - d.mark(r.Sym, nil) - } + for i := range exports.R { + d.mark(exports.R[i].Sym, nil) + } 

除了只在每次迭代中复制R[i] ,我们仅求助于我们感兴趣的唯一成员Sym


 name old time/op new time/op delta Linker-4 530ms ± 2% 521ms ± 3% -1.80% (p=0.000 n=17+20) 

完整版本的修补程序可在以下位置获得: CL113636


namedConst


不幸的是,在Go中,命名常量(即使组装成组)也不会相互连接,也不会形成枚举( 建议:spec:添加有类型的枚举支持 )。


一个问题是将无类型的常量强制转换为您想用作枚举的类型。


假设您定义了一种Color类型,其值为const ColDefault Color = 0
您更喜欢以下两个代码片段中的哪一个?


 // (A) if color == 0 { return colorBlack } // (B) if color == colorDefault { return colorBlack } 

如果(B)更适合您,则检查namedConst将帮助您绕过命名常量本身来跟踪命名常量值的使用。


这就是html/template包中的context.mangle方法的转换方式:


  s := templateName + "$htmltemplate_" + c.state.String() - if c.delim != 0 { + if c.delim != delimNone { s += "_" + c.delim.String() } - if c.urlPart != 0 { + if c.urlPart != urlPartNone { s += "_" + c.urlPart.String() } - if c.jsCtx != 0 { + if c.jsCtx != jsCtxRegexp { s += "_" + c.jsCtx.String() } - if c.attr != 0 { + if c.attr != attrNone { s += "_" + c.attr.String() } - if c.element != 0 { + if c.element != elementNone { s += "_" + c.element.String() } return s 

顺便说一句,有时在补丁的链接上您可以找到有趣的讨论...
CL123376就是这样一种情况。


取消切片


切片表达式的一个特征是,如果类型x是切片或字符串,则x[:]始终与x相同。 对于切片,这适用于任何类型的[]T元素。


下表中的所有内容都相同( x切片):


  • x
  • x[:]
  • x[:][:]
  • ...

切片找到类似的冗余切片表达式。 这些表达是有害的,首先,这会带来额外的认知负担。 x[:]在从数组中获取切片的情况下具有相当重要的语义。 具有默认范围的切片除了噪声以外不会执行任何操作。


我要求在CL123375补丁。


switchTrue


CL123378中 ,“ switch true {...} ”被“ switch {...} ”代替。
两种形式都是等效的,但第二种形式更惯用。
通过检查switchTrue发现。


大多数样式检查都精确地揭示了两种情况都可接受的情况,但其中一种更为常见,也为更多Go程序员所熟悉。 下次检查同一系列。


typeUnparen


像许多其他编程语言一样,Go也喜欢括号。 如此之多,以至于我准备接受其中任何一个:


 type ( t0 int t1 (int) t2 ((int)) // ... ,  . ) 

但是,如果您运行gofmt会发生什么?


 type ( t0 int t1 (int) // <- !   . t2 (int) // <-    ... ) 

这就是为什么typeUnparen存在。 他在程序中找到了所有类型表达式,您可以在其中减少方括号的数量。 我尝试发送CL123379 ,让我们看看社区如何接受它。


Lisp不喜欢大括号

与类似C的语言不同,在Lisp中,在任何地方插入无用的括号并不容易。 因此,在语法基于S表达式的语言中 ,编写一个除了括号以外不执行任何操作的程序比在某些其他语言中更困难。


随时随地批评



我们仅检查了已实施检查的一小部分。 同时,它们的数量和质量只会随着时间的推移而发展,这要感谢那些参与开发的人


go-critic绝对免费的( MIT许可证 ),并且对您参与该项目的开发也开放。 向我们发送检查建议,您可以立即进行实施,报告发现的错误和缺点,分享您的印象。 您还可以在Go代码审查中提出要审计或报告的项目,这种经验对我们来说是无价的。


参与主题


您看过俄罗斯的Go贡献研讨会文章吗? 今年秋天将是第二轮。 这次,除了更成功的格式和赞助商之外,我们还将拥有一个秘密武器-出色的静态分析器。 将会有足够的贡献!


但实际上,您可以立即开始(尽管更好-在代码冻结之后再过一会儿)。 如果您设法在下一次研讨会之前感到舒适,那将非常酷,因为我们在俄罗斯缺少导师。

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


All Articles