Go 部落格
使用 deadcode 找出無法存取的函式
存在於專案原始碼,但無法在任何執行項目中存取的函式稱為「dead code」,而且也會拖累程式碼庫的維護工作。我們很高興今天能與您分享一個名為 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 的結構進行許多更動,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
方法需要它,而 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
存取,因此其方法現在可能是動態調用的目標。
這會導致先有雞還是先有蛋的情況。當我們遇到每個新的可存取函式時,我們會發現更多介面方法調用和更多從具體類型到介面類型的轉換。但隨著這兩個集合(介面方法調用 × 具體類型)的交叉乘積不斷擴大,我們會發現新的可存取函式。這種稱為「動態規劃」的問題類別可以透過(在概念上)在大型的二維表中做出記號,在進行時新增列和欄,直到沒有記號需要新增為止來解決。最終表中的記號會告知我們哪些是可以存取的;空白格則是無效程式碼。
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 開發的回饋
前一篇文章:2023 年 H2 Go 開發者調查結果
Blog 目錄