Go中的静态分析:如何在检查代码时节省时间


哈Ha 我叫Sergey Rudachenko,我是Roistat的技术专家。 在过去的两年中,我们的团队一直在将项目的各个部分转换为Go微服务。 它们是由几个团队开发的,因此我们需要设置硬代码质量标准。 为此,我们使用了几种工具,在本文中,我们将重点介绍其中一种工具-静态分析。


静态分析是使用特殊工具自动检查源代码的过程。 本文将讨论其好处,简要介绍流行的工具并提供实施说明。 如果您根本没有遇到过类似的工具或没有系统地使用它们,则值得阅读。


在有关此主题的文章中,经常会找到“ linter”一词。 对于我们来说,这是用于静态分析的简单工具的方便名称。 linter的任务是搜索简单的错误和不正确的设计。


为什么需要短绒呢?


在团队中工作时,您最有可能执行代码审查。 审查中跳过的错误是潜在的错误。 错过了未处理的error -不会收到提示性消息,您将盲目查找问题。 错误地进行类型转换或转换为nil map-更糟糕的是,二进制文件会因恐慌而掉线。


上面描述的错误可以添加到代码约定中 ,但是在读取请求请求时查找它们并不是那么简单,因为审阅者必须读取代码。 如果您的头脑中没有编译器,那么无论如何,有些问题还是会解决。 另外,对较小错误的搜索分散了检查逻辑和体系结构的注意力。 在某种程度上,支持这样的代码将变得更加昂贵。 我们用静态类型的语言编写,奇怪的是不使用它。


热门工具


大多数用于静态分析的工具都使用go/astgo/parser软件包。 它们提供了解析.go文件语法的功能。 标准执行线程(例如,用于golint实用程序)如下:


  • 所需软件包的文件列表已加载
  • 对每个文件执行parser.ParseFile(...) (*ast.File, error)
  • 检查每个文件或包的受支持规则
  • 验证通过每条指令,例如,如下所示:

 f, err := parser.ParseFile(/* ... */) ast.Walk(func (n *ast.Node) { switch v := node.(type) { case *ast.FuncDecl: if strings.Contains(v.Name, "_") { panic("wrong function naming") } } }, f) 

除了AST,还有单一静态分配(SSA)。 这是解析与执行线程(而不是语法构造)一起工作的代码的更复杂的方式。 在本文中,我们不会对其进行详细考虑,您可以阅读文档并查看stackcheck实用程序示例


接下来,将仅考虑对我们执行有用检查的流行实用程序。


gofmt


这是go软件包中的标准实用程序,用于检查样式匹配并可以自动对其进行修复。 遵守样式是我们的强制性要求,因此gofmt验证包含在我们的所有项目中。


类型检查


Typecheck检查代码中的类型匹配并支持供应商(与gotype不同)。 需要启动它才能检查编译,但不能完全保证。


去兽医


Go vet实用程序是标准软件包的一部分,建议Go团队使用。 检查许多常见错误,例如:


  • 滥用printf和类似功能
  • 错误的构建标签
  • 比较功能和零

lin


Golint由Go团队开发,并根据Effective GoCodeReviewComments文档验证代码。 不幸的是,没有详细的文档,但是代码显示选中了以下内容:


 f.lintPackageComment() f.lintImports() f.lintBlankImports() f.lintExported() f.lintNames() f.lintVarDecls() f.lintElses() f.lintRanges() f.lintErrorf() f.lintErrors() f.lintErrorStrings() f.lintReceiverNames() f.lintIncDec() f.lintErrorReturn() f.lintUnexportedReturn() f.lintTimeNames() f.lintContextKeyTypes() f.lintContextArgs() 

静态检查


开发人员自己将staticcheck作为改进的兽医。 有很多检查,它们分为几组:


  • 滥用标准库
  • 多线程问题
  • 测试问题
  • 无用的代码
  • 性能问题
  • 可疑的设计

s


它专门研究寻找值得简化的结构,例如:


之前golint源代码


 func (f *file) isMain() bool { if ffName.Name == "main" { return true } return false } 

之后


 func (f *file) isMain() bool { return ffName.Name == "main" } 

该文档类似于staticcheck,并包含详细的示例。


错误检查


函数返回的错误不能忽略。 原因在装订文件Effective Go中有详细描述。 Errcheck将不会跳过以下代码:


 json.Unmarshal(text, &val) f, _ := os.OpenFile(/* ... */) 


查找代码中的漏洞:硬编码访问,SQL注入和不安全哈希函数的使用。


错误示例:


 //    IP  l, err := net.Listen("tcp", ":2000") //  sql  q := fmt.Sprintf("SELECT * FROM foo where name = '%s'", name) q := "SELECT * FROM foo where name = " + name //     import "crypto/md5" 

恶心的


在Go中,结构中字段的顺序会影响内存消耗。 Maligned查找非最佳排序。 使用此字段顺序:


 struct { a bool b string c bool } 

由于在字段a和c之后添加了空位,因此该结构将在内存中占用32位。


图片


如果我们更改排序并将两个布尔字段放在一起,则该结构将仅占用24位:


图片


Stackoverflow上的原始图像


Goconst


代码中的不可思议的变量不能反映含义并使阅读变得复杂。 Goconst查找在代码中出现2次或更多次的文字和数字。 请注意,通常即使一次使用也可能是一个错误。


Gocyclo


我们认为代码的循环复杂度是一个重要的指标。 Gocycle显示了每个功能的复杂性。 只能显示超过指定值的功能。


 gocyclo -over 7 package/name 

我们为自己选择了一个阈值7,因为我们没有找到不需要重构的复杂性更高的代码。


死码


有几种实用程序可用于查找未使用的代码;它们的功能可能会部分重叠。


  • ineffassign:检查无用的分配

 func foo() error { var res interface{} log.Println(res) res, err := loadData() //  res    return err } 

  • 死代码:查找未使用的函数
  • 未使用:查找未使用的函数,但比死代码更好

 func unusedFunc() { formallyUsedFunc() } func formallyUsedFunc() { } 

结果,未使用的将立即指向两个函数,而死代码仅指向未使用的功能。 因此,一次删除了多余的代码。 未使用的也会查找未使用的变量和结构字段。


  • varcheck:查找未使用的变量
  • 取消转换:查找无用的类型转换

 var res int return int(res) // unconvert error 

如果没有任务可以节省启动检查的时间,则最好将它们全部运行在一起。 如果需要优化,我建议使用未使用和未转换的文件。


配置有多方便


依次运行上述工具很不方便:错误是以不同的格式发布的,执行需要很多时间。 检查大约8000行代码大小的我们的一项服务花费了超过两分钟的时间。 您还必须单独安装实用程序。


有聚合工具可以解决此问题,例如goreportergometalinter 。 Goreporter使用html呈现报告,而gometalinter写入控制台。


Gometalinter仍在某些大型项目中使用(例如docker )。 他知道如何使用单个命令安装所有实用程序,并行运行它们以及根据模板格式化错误。 上述服务的执行时间减少到一分半钟。


聚合仅通过错误文本的确切重合起作用,因此,输出中不可避免地会出现重复错误。


2018年5月, golangci-lint项目出现在github上,在便利性上大大超越了gometalinter:


  • 同一项目的执行时间减少到16秒(8次)
  • 几乎没有重复的错误
  • 清除yaml配置
  • 很好的错误输出,带有一行代码和一个指向问题的指针
  • 无需安装其他实用程序

现在,速度的提高是通过重用SSA和loader.Program来实现的 ,将来还计划重用AST树,这是我在“工具”部分开始时写的。


hub.docker.com上撰写本文时,没有包含文档的图像,因此我们根据对方便的想法进行了定制。 将来,配置将更改,因此对于生产环境,我们建议您用自己的配置替换。 为此,只需将.golangci.yaml文件添加到项目的根目录(golangci-lint存储库中就是一个示例 )。


 PACKAGE=package/name docker run --rm -t \ -v $(GOPATH)/src/$(PACKAGE):/go/src/$(PACKAGE) \ -w /go/src/$(PACKAGE) \ roistat/golangci-lint 

此命令可以测试整个项目。 例如,如果它在~/go/src/project ,则将变量的值更改为PACKAGE=project 。 验证在所有内部包上递归进行。


请注意,此命令仅在使用供应商时才能正确运行。


实作


我们所有的开发服务都使用docker。 任何项目都在未安装go环境的情况下运行。 要运行命令,请使用Makefile并向其中添加lint命令:


 lint: @docker run --rm -t -v $(GOPATH)/src/$(PACKAGE):/go/src/$(PACKAGE) -w /go/src/$(PACKAGE) roistat/golangci-lint 

现在使用以下命令开始检查:


 make lint 

有一种简单的方法可以阻止带有错误的代码进入主服务器-创建一个预接收钩子。 适用于以下情况:


  1. 您的项目很小,依赖项很少(或者它们在存储库中)
  2. 等待几分钟来执行git push命令不是问题

挂钩配置说明: GitlabBitbucket服务器Github Enterprise


在其他情况下,最好使用CI并禁止存在至少一个错误的合并代码。 我们就是这样做的,在测试之前添加了linter的启动。


结论


引入系统评价大大缩短了评审时间。 但是,另一件事更为重要:现在,我们大多数时间可以讨论全局和体系结构。 这使您可以考虑项目的开发,而不是堵塞漏洞。

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


All Articles