詳解Go語(yǔ)言中如何高效遍歷目錄
目錄遍歷是一個(gè)很常見(jiàn)的操作,它的使用場(chǎng)景有如文件目錄查看(最典型的應(yīng)用如 ls 命令)、文件系統(tǒng)清理、日志分析、項(xiàng)目構(gòu)建等。
本文將嘗試逐步介紹在 Go 中幾種遍歷目錄文件的方法,從傳統(tǒng)的 ioutil.ReadDir 函數(shù)開(kāi)始,逐漸深入。
文中也會(huì)提供示例代碼、提供一些性能剖析,以便于大家更好地理解。
ioutil.ReadDir
首先,Go 中目錄文件遍歷的第一種方式是 ioutil.ReadDir
函數(shù)。
在 Go 1.16 版本前,ioutil.ReadDir
就是遍歷目錄的標(biāo)準(zhǔn)方法,它的返回結(jié)構(gòu)是目錄中文件的 FileInfo
列表,簡(jiǎn)單直接。
示例代碼:
func main() { files, err := ioutil.ReadDir(".") if err != nil { log.Fatal(err) } for _, f := range files { fmt.Println(f.Name()) } }
但它的缺點(diǎn)也非常明顯,性能不高。導(dǎo)致它的主要原因有如下幾點(diǎn):
完全加載
這就導(dǎo)致了 ioutil.ReadDir
在返回結(jié)果前,會(huì)將目錄下所有文件的信息完全加載到內(nèi)存中。對(duì)于包含大量文件的目錄,它就需要在內(nèi)存中存儲(chǔ)大量的 FileInfo 對(duì)象,毫無(wú)疑問(wèn),這會(huì)增加內(nèi)存使用。
FileInfo 開(kāi)銷
由于是完全加載,每個(gè) FileInfo 對(duì)象都包含了文件的詳細(xì)信息,如文件名、大小、修改時(shí)間等都會(huì)在返回之前都已經(jīng)加載完成。但獲取這些信息需進(jìn)行系統(tǒng)調(diào)用。而每個(gè)文件都要做這樣的調(diào)用,當(dāng)文件數(shù)量很多時(shí),這些系統(tǒng)調(diào)用的累積開(kāi)銷可以變得不容忽視了。
無(wú)法分批處理
由于 ioutil.ReadDir
是一次性返回所有文件信息,沒(méi)有提供分批處理的能力。無(wú)論目錄中有多少文件,都要等待所有文件信息讀取完成,這在處理目錄中包含大量文件的場(chǎng)景中,也就無(wú)法提前并行處理,效率是可想而知的。
這一點(diǎn)其實(shí)和我們前面的一篇文章,介紹的 GO 中按行(或者說(shuō)按塊)讀取文件的邏輯是類似的,一次加載全部?jī)?nèi)容,有潛在的性能問(wèn)題。
由于 ioutil.ReadDir
有這么多的缺點(diǎn),所以它在 Go 1.16 及更高版本已經(jīng)被棄用了。
那現(xiàn)在我們?cè)撚檬裁捶椒兀?/p>
os.ReadDir
從 Go 1.16 版本起,標(biāo)準(zhǔn)庫(kù)針對(duì)目錄遍歷查看提供了新的函數(shù) os.ReadDir
,以用來(lái)簡(jiǎn)化和提高遍歷目錄文件的效率。
函數(shù)簽名如下:
func ReadDir(name string) ([]DirEntry, error)
os.ReadDir
函數(shù)返回一個(gè)按文件名排序的 DirEntry
類型切片。如果在讀取目錄項(xiàng)時(shí)遇到錯(cuò)誤,它也會(huì)盡量返回已讀取內(nèi)容。這種設(shè)計(jì)同時(shí)兼顧了效率和錯(cuò)誤處理的需要。
示例代碼:
func main() { files, err := os.ReadDir(".") if err != nil { log.Fatal(err) } for _, file := range files { fmt.Println(file.Name()) } }
os.ReadDir
相比于舊方法 ioutil.ReadDir
的有什么優(yōu)勢(shì)?為什么丟棄 ioutil.ReadDir
而引入這個(gè)新的 os.ReadDir
。
如果對(duì)比兩者源碼,會(huì)發(fā)現(xiàn)差異主要在返回的類型上。os.ReadDir
返回的 []DirEntry
而非 []FileInfo
。它還具有性能優(yōu)勢(shì)。
為什么?
因?yàn)?nbsp;DirEntry
允許按需獲取文件詳情,即懶加載,而非是遍歷目錄時(shí)立即加載所有文件屬性。很多場(chǎng)景下,我們并不需要
我在 MacOS 系統(tǒng)下測(cè)試的 DirEntry
接口的實(shí)際變量類型為 os.unixDirent
。
它的源碼如下:
func (d *unixDirent) Name() string { return d.name } func (d *unixDirent) IsDir() bool { return d.typ.IsDir() } func (d *unixDirent) Type() FileMode { return d.typ } func (d *unixDirent) Info() (FileInfo, error) { if d.info != nil { return d.info, nil } return lstat(d.parent + "/" + d.name) }
我們只有在調(diào)用 Info
方法時(shí),才會(huì)真正通過(guò) lstat
發(fā)起系統(tǒng)調(diào)用。
如果你有將舊代碼遷移到 DirEntry
的需求, Go 1.17 還引入了 fs.FileInfoToDirEntry
函數(shù),允許我們將 FileInfo
對(duì)象轉(zhuǎn)換為 DirEntry
對(duì)象。
info, _ := os.Stat("somefile") dirEntry := fs.FileInfoToDirEntry(info)
看到這,對(duì)于認(rèn)真思考的朋友,或許已經(jīng)發(fā)現(xiàn)我們還有一個(gè)問(wèn)題沒(méi)解決,即 os.ReadDir
不是也不支持分批處理的能力嗎?
繼續(xù)往下看吧,我將介紹一個(gè)更底層的方法。
os.File 的 ReadDir 方法
我們知道 os.Open
是用于打開(kāi)文件的,但其實(shí)它也可用于打開(kāi)目錄。如果 os.Open
打開(kāi)的是目錄,我們?cè)谒祷氐?nbsp;os.File
上調(diào)用 ReadDir
以查看目錄內(nèi)容。
示例代碼:
func main() { dir, err := os.Open(".") if err != nil { log.Fatal(err) } defer dir.Close() files, err := dir.ReadDir(-1) if err != nil { log.Fatal(err) } for _, file := range files { fmt.Println(file.Name()) } }
如上的代碼其實(shí)類似于 os.ReadDir
內(nèi)容的實(shí)現(xiàn)代碼。
os.ReadDir
源碼如下:
func ReadDir(name string) ([]DirEntry, error) { f, err := Open(name) if err != nil { return nil, err } defer f.Close() dirs, err := f.ReadDir(-1) sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() }) return dirs, err }
這種方法更底層,提供了更多的靈活性。我們就可以用它分批讀取目標(biāo)。
如何實(shí)現(xiàn)呢?
核心就是那句的 dir.ReadDir(-1)
,它的入?yún)⒅付嗣看巫x取文件的數(shù)量,而 -1
表示讀取目錄的所有內(nèi)容。我們只要將 -1 改為分批讀取的數(shù)量即可,多次循環(huán)即可。
示例代碼:
func main() { dir, err := os.Open(".") if err != nil { log.Fatal(err) } defer dir.Close() for { files, err := dir.ReadDir(10) // 每批讀取10個(gè)條目 if err == io.EOF { break // 遍歷完成 } if err != nil { log.Fatal(err) // 處理其他錯(cuò)誤 } for _, file := range files { fmt.Println(file.Name()) } } }
這段代碼演示了如何使用 File.ReadDir
分批處理目錄中的文件。通過(guò)這種方式,可以更有效地管理內(nèi)存使用。
補(bǔ)充一點(diǎn)
在寫這篇文章時(shí),我發(fā)現(xiàn) os.File
有兩個(gè)查看目錄的方法,分別是 Readdir
和 ReadDir
。功能上的區(qū)別是新的 ReadDir
返回的是 []DirEntry
,而 Readdir
返回的是 []FileInfo
。
換句話說(shuō),ReadDir 本質(zhì)上是 Readdir 的升級(jí)版。
它們的函數(shù)簽名,如下所示:
func (f *File) Readdir(n int) ([]FileInfo, error) func (f *File) ReadDir(n int) ([]DirEntry, error)
這算是不支持可選參數(shù)和重載,但要解決兼容問(wèn)題采取的措施嗎?真的是蚌埠住了。
目錄的遞歸遍歷
現(xiàn)在,還差最后一個(gè)內(nèi)容沒(méi)有介紹,那就是遞歸目錄遍歷。
針對(duì)目錄的遞歸遍歷,Go 中提供了一個(gè)專門的函數(shù),filepath.Walk
。它可以遍歷指定目錄下的所有子目錄。
示例代碼:
func main() { err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error { if err != nil { return err } fmt.Println(path) return nil }) if err != nil { fmt.Printf("error walking the path %v: %v\n", ".", err) } }
我們通過(guò)遍歷的回調(diào)函數(shù)中在處理每個(gè)文件。它簡(jiǎn)化了目錄的遞歸遍歷,但對(duì)于大型或深層次的目錄結(jié)構(gòu),同樣存在著提前加載 FileInfo 的問(wèn)題。
針對(duì)這個(gè)問(wèn)題,在 Go1.16 版本也引入了基于 DirEntry
版的 filepath.WalkDir
函數(shù)。
filepath.WalkDir
的函數(shù)簽名如下:
func WalkDir(root string, fn fs.WalkDirFunc) error
fs.WalkDirFunc
的定義如下:
type WalkDirFunc func(path string, d DirEntry, err error) error
新函數(shù)的遍歷回調(diào)參數(shù)是 DirEntry
,而非 FileInfo
。現(xiàn)在,filepath.WalkDir
也有了延遲加載 FileInfo
的能力了。
現(xiàn)在,我們?cè)賮?lái)看下這張圖。
總結(jié)
在本文中,我們系統(tǒng)介紹了 Go 中多種遍歷目錄文件的方法。從傳統(tǒng)的 ioutil.ReadDir
,到 Go 1.16 引入的 os.ReadDir
,os.File
的 ReadDir
方法。每種方法適用于不同的場(chǎng)景,如何選擇要取決于你的需求、Go 版本、性能。如果你需要遞歸遍歷,也可以使用基于 DirEntry
的 filepath.WalkDir
實(shí)現(xiàn),提高遍歷的性能。
以上就是詳解Go語(yǔ)言中如何高效遍歷目錄的詳細(xì)內(nèi)容,更多關(guān)于Go遍歷目錄的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
淺析Go語(yǔ)言如何在終端里實(shí)現(xiàn)倒計(jì)時(shí)
這篇文章主要為大家詳細(xì)介紹了Go語(yǔ)言中是如何在終端里實(shí)現(xiàn)倒計(jì)時(shí)的,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-03-03Goland使用Go Modules創(chuàng)建/管理項(xiàng)目的操作
這篇文章主要介紹了Goland使用Go Modules創(chuàng)建/管理項(xiàng)目的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-05-05Golang開(kāi)發(fā)中如何解決共享變量問(wèn)題
Go提供了傳統(tǒng)通過(guò)共享變量,也就是共享內(nèi)存的方式來(lái)實(shí)現(xiàn)并發(fā)。這篇文章會(huì)介紹 Go提供的相關(guān)機(jī)制,對(duì)Golang共享變量相關(guān)知識(shí)感興趣的朋友一起看看吧2021-09-09如何解決goland,idea全局搜索快捷鍵失效問(wèn)題
這篇文章主要介紹了如何解決goland,idea全局搜索快捷鍵失效問(wèn)題,快捷鍵失效,可能是快捷鍵沖突,也或者是快捷鍵被修改成其他了。在settings下查看快捷鍵是否被修改,下文詳細(xì)介紹需要的朋友可以參考下2022-04-04golang多維度排序及題解最長(zhǎng)連續(xù)序列
這篇文章主要為大家介紹了golang多維度排序及題解最長(zhǎng)連續(xù)序列示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10