Go 部落格

使用 deadcode 查詢不可達函式

Alan Donovan
2023 年 12 月 12 日

您專案原始碼的一部分,但在任何執行中都永遠無法到達的函式被稱為“死程式碼”,它們會拖累程式碼庫的維護工作。今天我們很高興分享一個名為 deadcode 的工具來幫助您識別它們。

$ go install golang.org/x/tools/cmd/deadcode@latest
$ deadcode -help
The deadcode command reports unreachable functions in Go programs.

Usage: deadcode [flags] package...

示例

在過去一年左右的時間裡,我們對 gopls(為 VS Code 和其他編輯器提供支援的 Go 語言伺服器)的結構進行了大量更改。典型的更改可能會重寫現有函式,並仔細確保其新行為滿足所有現有呼叫者的需求。有時,在付出所有努力後,我們會沮喪地發現,其中一個呼叫者實際上從未在任何執行中被呼叫,因此可以安全地將其刪除。如果事先知道這一點,我們的重構任務就會更容易。

下面的簡單 Go 程式說明了這個問題

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  implements Greeter
var _ Greeter = Goodbyer{} // Goodbyer implements 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 方法需要,而該方法又需要實現 Greeter 介面,而 Greeter 介面的 Greet 方法被 main 呼叫。但是,如果我們從 main 向前工作,我們可以看到從未建立任何 Goodbyer 值,因此 main 中的 Greet 呼叫只能到達 Helloer.Greet。這就是 deadcode 工具使用的演算法背後的想法。

當我們在此程式上執行 deadcode 時,該工具告訴我們 goodbye 函式和 Goodbyer.Greet 方法都不可達

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

有了這些知識,我們就可以安全地刪除這兩個函式以及 Goodbyer 型別本身。

該工具還可以解釋為什麼 hello 函式是活動的。它會響應一個函式呼叫鏈,該呼叫鏈從 main 開始到達 hello

$ 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 命令 載入解析型別檢查 指定的包,然後將它們轉換為與典型編譯器類似的 中間表示

然後,它使用一種稱為 快速型別分析 (RTA) 的演算法來構建可達函式集,該函式集最初只是每個 main 包的入口點:main 函式和包初始化函式,後者分配全域性變數並呼叫名為 init 的函式。

RTA 檢查每個可達函式體中的語句,以收集三種資訊:它直接呼叫的函式集;它透過介面方法進行的動態呼叫集;以及它轉換為介面的型別集。

直接函式呼叫很簡單:我們只需將被呼叫者新增到可達函式集中,如果我們是第一次遇到被呼叫者,我們會像處理 main 一樣檢查其函式體。

透過介面方法的動態呼叫比較棘手,因為我們不知道實現該介面的型別集。我們不想假設程式中所有可能的匹配型別的型別都是呼叫可能的,因為其中一些型別可能只從死程式碼中例項化!這就是為什麼我們收集轉換為介面的型別集:轉換使這些型別中的每一種都可從 main 到達,因此其方法現在是動態呼叫的可能目標。

這會導致一個雞生蛋還是蛋生雞的問題。當我們遇到每個新的可達函式時,我們會發現更多的介面方法呼叫和更多的具體型別轉換為介面型別。但是,隨著這兩個集合的交叉乘積(介面方法呼叫 × 具體型別)不斷增大,我們發現了新的可達函式。這類問題稱為“動態規劃”,可以透過(概念上)在一個大型二維表中做標記來解決,隨著進行新增行和列,直到沒有更多的標記可新增。最終表格中的標記告訴我們什麼可達;空白單元格就是死程式碼。

illustration of Rapid Type Analysis
main 函式導致 Helloer 被例項化,並且 g.Greet 呼叫
將分派到迄今為止已例項化的每種型別的 Greet 方法。

對(非方法)函式的動態呼叫被視為類似於單個方法的介面。並且 使用反射 進行的呼叫被認為會到達介面轉換中使用的任何型別的任何方法,或使用 reflect 包從一個型別派生的任何型別。但原則上所有情況都是一樣的。

測試

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

-filter 標誌將輸出限制為與正則表示式匹配的包。預設情況下,該工具會報告初始模組中的所有包。)

可靠性

所有靜態分析工具 必然 會產生對目標程式可能動態行為的不完美近似。工具的假設和推斷可能是“可靠的”,意味著保守但可能過於謹慎,或者“不可靠的”,意味著樂觀但並非總是正確。

deadcode 工具也不例外:它必須近似函式和介面值或使用反射進行的動態呼叫的目標集。在這方面,該工具是可靠的。換句話說,如果它將某個函式報告為死程式碼,則意味著該函式即使透過這些動態機制也無法被呼叫。但是,該工具可能無法報告一些實際上永遠無法執行的函式。

deadcode 工具還必須近似用 Go 以外的語言編寫的函式進行的呼叫集,這些呼叫它看不到。在這方面,該工具並不可靠。它的分析不知道僅從彙編程式碼呼叫的函式,也不知道由 go:linkname 指令 引起的函式別名。幸運的是,除了 Go 執行時之外,這些功能很少使用。

試用一下

我們會定期在我們的專案上執行 deadcode,尤其是在重構工作之後,以幫助識別程式中不再需要的程式碼部分。

安息了死程式碼,您就可以專注於消除那些本已過時但卻頑固地活著的程式碼,繼續消耗您的生命力。我們將這類不死函式稱為“吸血鬼程式碼”!

請試用一下

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

我們發現它很有用,希望您也如此。

下一篇文章:分享您對 Go 開發的反饋
上一篇文章:Go 開發者調查 2023 年下半年結果
部落格索引