源碼分析Go語言中g(shù)ofmt實現(xiàn)原理
前言
gofmt
是 Go 語言官方提供的一個工具,用于自動格式化 Go 源代碼,使其符合 Go 語言的官方編碼風格。其實現(xiàn)原理大致可以分為以下幾個步驟:
讀取和解析源代碼:
gofmt
首先讀取指定的 Go 源代碼文件或目錄。對于目錄,它會遞歸地查找所有的.go
文件。然后,它使用 Go 語言的解析庫(如go/parser
和go/token
)來解析源代碼,并構(gòu)建出一個抽象語法樹(AST)。遍歷抽象語法樹(AST):一旦源代碼被解析成 AST,
gofmt
會遍歷這棵樹。這個過程中,它會識別和修改樹中的節(jié)點,以便按照 Go 語言的格式化規(guī)則調(diào)整代碼的格式。這包括調(diào)整縮進、空格、換行符等。格式化規(guī)則應(yīng)用:
gofmt
通過內(nèi)置的一系列格式化規(guī)則來調(diào)整和優(yōu)化代碼的布局。這些規(guī)則涵蓋了從基本的縮進和行長度控制到更復雜的代碼結(jié)構(gòu)對齊等方面。gofmt
的目標是保持代碼風格的一致性,以提高代碼的可讀性和維護性。生成和輸出格式化后的代碼:遍歷和修改 AST 后,
gofmt
將根據(jù)修改后的 AST 生成格式化后的源代碼。這一步是通過 AST 的序列化完成的,即將 AST 轉(zhuǎn)換回 Go 語言源代碼的文本表示。覆蓋原始文件或輸出到標準輸出:默認情況下,
gofmt
會將格式化后的代碼輸出到標準輸出(屏幕)。如果指定了-w
(write)選項,gofmt
會將格式化后的代碼寫回原始文件,替換掉原來的內(nèi)容。還有其他選項可以用來控制輸出,例如-d
選項會顯示格式化前后的差異。
gofmt
的設(shè)計哲學是“無需配置”,旨在為 Go 社區(qū)提供一個統(tǒng)一的代碼格式標準。通過自動格式化代碼,gofmt
減少了代碼審查中關(guān)于風格的討論,使開發(fā)者可以更專注于代碼的邏輯和功能上。
源碼分析
以目前最新的go
穩(wěn)定版1.22.1
為例,gofmt
庫的源碼文件如下所示,共計8個文件和一個測試數(shù)據(jù)文件夾:
調(diào)用流程
我們先來預覽下gofmt
庫的函數(shù)調(diào)用流程圖
源碼處窺
下面我們通過閱讀源碼直觀的感受下gofmt
究竟是怎么工作的。
gofmt.go
則是gofmt
庫的核心,也是它的入口,所以我們從這個文件開始看起。 在該文件開頭可以看到gofmt
的flag
定義:
入口函數(shù)在358
行,在370
行又調(diào)用了真正負責格式化處理流程的gofmtMain
:
gofmtMain
中的關(guān)鍵函數(shù)是processFile
,不過它的運行需要依賴當前文件中的initParserMode()
以及rewrite.go中的initRewrite()
這兩個init
函數(shù)我們按下不表,因為他們不是我們本文的重點,有興趣的同學可以按圖索驥自行查看其實現(xiàn)。我們回到processFile
函數(shù),繼續(xù)查看其核心執(zhí)行邏輯。
processFile
讀取文件后,調(diào)用internal.go
中的parse
函數(shù)進行語法解析,然后依賴simplify.go
中的simplify
生成抽象語法樹。在這兩個文件中我們終于看到【大致流程】中提到的go/parser
和 go/token
兩個關(guān)鍵庫,其實另外一個go/ast
也很重要,它主要負責抽象語法樹相關(guān)的工作。
gofmt.go重點依賴的庫
go/token
功能作用:go/token
包定義了 Go 語言的詞法標記(tokens)及其位置信息(文件位置)。它為語法分析和 AST 構(gòu)建提供基礎(chǔ)設(shè)施,包括標識符、關(guān)鍵字、文本位置等的表示。
應(yīng)用場景:這個包通常與 go/parser
和 go/ast
結(jié)合使用,用于實現(xiàn)對 Go 代碼的詞法分析和位置追蹤。應(yīng)用場景包括編譯器開發(fā)、代碼分析工具、代碼格式化工具等,任何需要精確控制和了解代碼結(jié)構(gòu)及元素位置的場合。
go/ast
功能作用:go/ast
包提供了構(gòu)造和操作 Go 語言源代碼的抽象語法樹(AST)的能力。AST 是源代碼的樹狀結(jié)構(gòu)表示,每個節(jié)點代表代碼的一部分,比如表達式、語句或聲明。
應(yīng)用場景:它主要用于開發(fā)需要分析、檢查、修改或生成 Go 代碼的工具,如靜態(tài)分析工具、代碼格式化程序、重構(gòu)工具等。
go/parser
功能作用:go/parser
包負責解析 Go 源代碼文件,將其轉(zhuǎn)化為 AST(抽象語法樹)。它提供了解析 Go 代碼的接口和功能,可以從文件、字符串等輸入源解析 Go 代碼。
應(yīng)用場景:用于需要讀取和解析 Go 源代碼的場合,比如編寫代碼編輯器的語法高亮功能、靜態(tài)代碼分析、文檔生成工具等。
format函數(shù)
通過閱讀processFile
源碼我們了解到,當源碼被解析為AST后,gofmt
執(zhí)行了一個format
函數(shù)來生成格式化后的結(jié)果。
res, err := format(fileSet, file, sourceAdj, indentAdj, src, printer.Config{Mode: printerMode, Tabwidth: tabWidth}) if err != nil { return err }
internal.go
中放置了fotmat
函數(shù)的實現(xiàn)。
其實format
函數(shù)才是gofmt
真正發(fā)揮威力的地方,畢竟讀文件、解析文件、生成語法樹、寫文件這些工作都是調(diào)用的別的庫,而format
才是它自己實打?qū)崒崿F(xiàn)的,我添加了詳細的中文注釋。
// format函數(shù)用于格式化給定的包文件,該文件最初從src獲得, // 并基于原始源代碼通過sourceAdj和indentAdj進行結(jié)果調(diào)整。 func format( fset *token.FileSet, // 文件集,用于存儲和處理文件的位置信息 file *ast.File, // 抽象語法樹表示的文件 sourceAdj func(src []byte, indent int) []byte, // 調(diào)整源代碼的函數(shù) indentAdj int, // 縮進調(diào)整量 src []byte, // 原始源代碼 cfg printer.Config, // 打印配置,控制輸出格式 ) ([]byte, error) { if sourceAdj == nil { // 如果沒有提供sourceAdj函數(shù),則處理完整的源文件 var buf bytes.Buffer // 創(chuàng)建一個緩沖區(qū)來存儲格式化后的代碼 err := cfg.Fprint(&buf, fset, file) // 將格式化后的文件輸出到緩沖區(qū) if err != nil { return nil, err // 如果發(fā)生錯誤,返回錯誤 } return buf.Bytes(), nil // 返回緩沖區(qū)中的字節(jié)切片 } // 處理部分源文件的情況 // 確定并添加前導空格 i, j := 0, 0 for j < len(src) && isSpace(src[j]) { // 遍歷源代碼前面的空白字符 if src[j] == '\n' { i = j + 1 // 更新最后一行前導空間的字節(jié)偏移量 } j++ } var res []byte // 用于存儲最終結(jié)果的切片 res = append(res, src[:i]...) // 將前導空格添加到結(jié)果中 // 確定并添加第一行代碼的縮進 // 除非沒有制表符,否則忽略空格,空格視為一個制表符 indent := 0 hasSpace := false for _, b := range src[i:j] { switch b { case ' ': hasSpace = true case '\t': indent++ // 統(tǒng)計制表符數(shù)量作為縮進量 } } if indent == 0 && hasSpace { indent = 1 // 如果沒有制表符但有空格,則將縮進設(shè)置為1 } for i := 0; i < indent; i++ { res = append(res, '\t') // 將縮進添加到結(jié)果中 } // 格式化源代碼 cfg.Indent = indent + indentAdj // 設(shè)置縮進配置 var buf bytes.Buffer err := cfg.Fprint(&buf, fset, file) // 將格式化的代碼寫入緩沖區(qū),不包括前導和尾隨空格 if err != nil { return nil, err } out := sourceAdj(buf.Bytes(), cfg.Indent) // 調(diào)整格式化后的代碼 // 如果調(diào)整后的輸出為空,則源代碼除了空白字符外是空的 // 結(jié)果是傳入的源代碼 if len(out) == 0 { return src, nil } // 否則,將輸出添加到前導空間后面 res = append(res, out...) // 確定并添加尾隨空格 i = len(src) for i > 0 && isSpace(src[i-1]) { i-- // 回溯以確定尾隨空格的開始位置 } return append(res, src[i:]...), nil // 將尾隨空格添加到結(jié)果中并返回 }
結(jié)語
至此我們得已窺到gofmt的廬山真面目。
對于想要了解gofmt
大體實現(xiàn)流程的同學來說,走到這里實屬不易,您已經(jīng)超越了只是知道怎么使用gofmt
的段位。
不過對于想要了解更多細節(jié)的同學來說,越往下挖疑問反而越多,比如:
- 用于調(diào)整源代碼的函數(shù)
sourceAdj
究竟是如何實現(xiàn)的? parser
、ast
、token
這些包內(nèi)部又是如何實現(xiàn)的?他們應(yīng)用了哪些基礎(chǔ)算法知識?
限于篇幅,我就不在本文作答以上問題了,我覺得能給大家?guī)硭伎己蛦l(fā)就是最好的分享。后續(xù)如果大家對于源碼解析很感興趣,我再作進一步的細節(jié)展開。
以上就是源碼分析Go語言中g(shù)ofmt實現(xiàn)原理的詳細內(nèi)容,更多關(guān)于Go gofmt實現(xiàn)原理的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
深入了解Go的interface{}底層原理實現(xiàn)
本文主要介紹了Go的interface{}底層原理實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2022-06-06