[Go official]使用 deadcode 寻找无法到达的函数与死代码

本文是由 Go Team 的 Alan Donovan 在 2023年12月12日发表于 go official blog,原文地址:https://go.dev/blog/deadcode

[Go official]使用 deadcode 寻找无法到达的函数与死代码

那些是你项目源代码的一部分但在任何执行中都无法到达的函数被称为“死代码”,这些代码在后期的维护当中可能会很麻烦。今天,我们很高兴分享一个名为 deadcode 的工具,帮助用户识别它们。

$ go install golang.org/x/tools/cmd/deadcode@latest
$ deadcode -help

deadcode 命令报告 Go 程序中无法到达的函数。

用法:deadcode [flags] package…

示例

在过去的一年左右,我们对 gopls 的结构进行了很多更改,gopls 是为 VS Code 和其他编辑器提供动力的 Go 语言服务器。一个典型的更改可能会重写一些现有的函数,小心确保其新行为满足所有现有调用者的需求。有时,在我们付出所有这些努力之后,我们会沮丧地发现其中一个调用者实际上在任何执行中都没有被达到,所以它可以安全地被删除。如果我们事先知道这一点,我们的重构任务会更容易。

下面的简单 Go 程序演示了 deadcode 的用法:

module example.com/greet
go 1.21
package main

import "fmt"

func main() {
    var g Greeter
    g = Helloer{}
    g.Greet()
}

type Greeter interface{ Greet() }

type Helloer struct{}
type Goodbyer struct{}

var _ Greeter = Helloer{}  // Helloer 实现 Greeter
var _ Greeter = Goodbyer{} // Goodbyer 实现 Greeter

func (Helloer) Greet()  { hello() }
func (Goodbyer) Greet() { goodbye() }

func hello()   { fmt.Println("hello") }
func goodbye() { fmt.Println("goodbye") }

当我们执行它时,它会说hello:

$ go run .
hello

从它的输出中可以清楚地看到,这个程序执行了 hello 函数,但没有执行 goodbye 函数。不太明显的是,goodbye 函数永远无法被调用。然而,我们不能简单地删除 goodbye,因为它是 Goodbyer.Greet 方法所必需的,而 Goodbyer.Greet 方法又需要实现 Greeter 接口,我们可以看到 Greeter 接口的 Greet 方法在 main 中被调用。但如果我们从 main 向前寻找,我们可以看到没有任何 Goodbyer 值被创建,所以 main 中的 Greet 调用只能到达 Helloer.Greet。这就是 deadcode 工具使用算法的思想。

当我们在这个程序上运行d eadcode 时,工具告诉我们 goodbye 函数和 Goodbyer.Greet 方法都是无法到达的:

$ deadcode .
greet.go:23: unreachable func: goodbye
greet.go:20: unreachable func: Goodbyer.Greet

有了这些信息,我们可以安全地删除这两个函数,以及 Goodbyer 类型本身。

这个工具还可以解释为什么 hello 函数是活动的。它回应了一系列函数调用链,这些调用链到达了 hello,从 main 开始:

$ deadcode -whylive=example.com/greet.hello .
                  example.com/greet.main
dynamic@L0008 --> example.com/greet.Helloer.Greet
 static@L0019 --> example.com/greet.hello

输出旨在在终端上易于阅读,但您可以使用-json或-f=template标志指定更丰富的输出格式,供其他工具使用。

deadcode 的工作原理

deadcode 命令加载、解析并类型检查指定的包,然后将它们转换为类似于典型编译器的中间表示。

然后它使用一种称为快速类型分析(RTA)的算法来构建一组可到达的函数,最初只是每个 main 包的入口点:main 函数和包初始化函数,该函数分配全局变量并调用名为 init 的函数。

RTA查看每个可到达函数体中的语句,以收集三种信息:它直接调用的函数集;它通过接口方法进行的动态调用集;以及它将类型转换为接口的类型集。

直接函数调用很容易:我们只需将被调用者添加到可到达函数集中,如果这是我们第一次遇到被调用者,我们就以与main相同的方式检查其函数体。

通过接口方法进行的动态调用更为棘手,因为我们不知道实现接口的类型集。我们不想假设程序中所有可能的方法,其类型匹配的就是可能的调用目标,因为其中一些类型可能仅从死代码中实例化!这就是为什么我们收集转换为接口的类型集:转换使这些类型从main中可到达,因此其方法现在是动态调用的可能目标。

这导致了一个先有鸡还是先有蛋的情况。当我们遇到每个新的可到达函数时,我们发现了更多的接口方法调用和更多的具体类型转换为接口类型。但随着这两个集合的乘积(接口方法调用×具体类型)不断增长,我们发现了新的可到达函数。这类问题被称为“动态规划”,可以通过(概念上)在一个大的二维表中做标记来解决,随着我们进行,添加行和列,直到没有更多的标记要添加。最终表中的标记告诉我们什么是可到达的;空白单元格是死代码。

[Go official]使用 deadcode 寻找无法到达的函数与死代码

Tests

RTA是全程序分析。这意味着它总是从main函数开始并向前工作:您不能从例如encoding/json这样的库包开始。

然而,大多数库包都有测试,测试有main函数。我们看不到它们,因为它们是在go test的幕后生成的,但我们可以使用-test标志将它们包含在分析中。

如果这报告说库包中的一个函数是死的,那是你的测试覆盖率可能需要提高的迹象。例如,此命令列出了所有未被其测试到达的encoding/json中的函数:

$ deadcode -test -filter=encoding/json encoding/json
encoding/json/decode.go:150:31: unreachable func: UnmarshalFieldError.Error
encoding/json/encode.go:225:28: unreachable func: InvalidUTF8Error.Error

所有静态分析工具必然产生目标程序可能的动态行为的不完美近似。工具的假设和推断可能是“声音的”,意味着保守但可能过于谨慎,或者“不声音的”,意味着乐观但并不总是正确的。

deadcode工具也不例外:它必须近似通过函数和接口值或使用反射进行的动态调用的目标集。在这方面,工具是声音的。换句话说,如果它报告一个函数为死代码,这意味着该函数甚至不能通过这些动态机制被调用。然而,工具可能无法报告一些实际上永远无法执行的函数。

deadcode 工具的分析不知道仅从汇编代码中调用的函数,或者由于 go:linkname 指令产生的函数别名。幸运的是,这两种特性在 Go 运行时之外很少使用(译者注:go 1.23 将会禁止使用 go:linkname)。

Try it out

我们定期在我们的项目上运行 deadcode,特别是在重构工作后,以帮助识别不再需要的程序部分。

安装试试看:

$ go install golang.org/x/tools/cmd/deadcode@latest


原文始发于微信公众号(Go Official Blog):[Go official]使用 deadcode 寻找无法到达的函数与死代码

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/295215.html

(0)
码上实战的头像码上实战

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!