原文地址:https://benhoyt.com/writings/go-readdir/node
原文做者:Ben Hoytgit
本文永久連接:github
https://github.com/gocn/translator/blob/golang
master/2021/w6_coming_in_go_1.16_readdir_web
and_direntry.md編程
譯者:cvley緩存
校對:guzzsek微信
2021年1月網絡
做爲Python中的 os.scandir
和 PEP 471 (scandir
的首次提案)的主要做者,我很開心看到將在2021年2月下旬發佈的Go 1.16版本中將增長相似的函數。app
在Go中,這個函數叫作 os.ReadDir
,是在去年九月提出的提案 。在100多個評論和對設計進行屢次細微調整後,Russ Cox 在10月提交了對應的代碼。此次提交也包含了一個不感知文件系統的版本,是位於新的io/fs
包中 fs.ReadDir
的函數。
爲何須要ReadDir?
簡短的答案是:性能。
當調用讀取文件夾路徑的系統函數時,操做系統通常會返回文件名_和_它的類型(在Windows下,還包括如文件大小和最後修改時間等的stat信息)。然而,原始版本的Go和Python接口會丟掉這些額外信息,這就須要在讀取每一個路徑時再多調用一個stat
。系統調用的性能較差 ,stat
可能從磁盤、或至少從磁盤緩存讀取信息。
在循環遍歷目錄樹時,你須要知道一個路徑是文件仍是文件夾,這樣才能夠知道循環遍歷的方式。所以即便一個簡單的目錄樹遍歷,也須要讀取文件夾路徑並獲取每一個路徑的stat
信息。但若是使用操做系統提供的文件類型信息,就能夠避免那些stat
系統調用,同時遍歷目錄的速度也將提升幾倍(在網絡文件系統上甚至能夠快十幾倍)。具體信息能夠參考Python版本的基準測試。
不幸的是,兩種語言中讀取文件夾的最初實現都不是最優的設計,不使用額外的系統調用stat
就沒法獲取類型信息:Python中的os.listdir
和Go中的 ioutil.ReadDir
。
我在2012年首次想到Python的scandir
背後的原理,併爲2015年發佈的Python 3.5實現了這個函數(從這裏能夠了解更多這個過程的信息)。此後這個函數不斷地被改進完善:好比,增長with
控制語句和文件描述符的支持。
對於Go語言,除了基於Python版本的經驗提出一些改進建議的評論外,我沒有參與這個提案或實現。
Python vs Go
咱們看下新的「讀取文件夾」的接口,尤爲關注下它們在Python和Go中有多麼的類似。
在Python中調用os.scandir(path)
,會返回一個os.DirEntry
的迭代器,以下所示:
class DirEntry:
# This entry's filename.
name: str
# This entry's full path: os.path.join(scandir_path, entry.name).
path: str
# Return inode or file ID for this entry.
def inode(self) -> int: ...
# Return True if this entry is a directory.
def is_dir(self, follow_symlinks=True) -> bool: ...
# Return True if this entry is a regular file.
def is_file(self, follow_symlinks=True) -> bool: ...
# Return True if this entry is a symbolic link.
def is_symlink(self) -> bool: ...
# Return stat information for this entry.
def stat(self, follow_symlinks=True) -> stat_result: ...
訪問name
和path
屬性將不會拋出異常,但根據操做系統和文件系統,以及路徑是否爲符號連接,方法的調用可能會拋出OSError
異常。好比,在Linux下,stat
老是會進行一次系統調用,所以可能會拋出異常,但is_X
的方法通常不會這樣。
在Go語言中,調用os.ReadDir(path)
,將會返回一個os.DirEntry
對象的切片,以下所示:
type DirEntry interface {
// Returns the name of this entry's file (or subdirectory).
Name() string
// Reports whether the entry describes a directory.
IsDir() bool
// Returns the type bits for the entry (a subset of FileMode).
Type() FileMode
// Returns the FileInfo (stat information) for this entry.
Info() (FileInfo, error)
}
儘管在真正的Go風格下,Go版本更加簡單,但你一眼就能夠看出兩者之間多麼類似。實際上,若是從新來寫Python的scandir
,我極可能會選擇一個更簡單的接口——尤爲是要去掉follow_symlinks
參數,不讓它默認跟隨處理符號連接。
下面是一個使用os.scandir
的例子——一個循環計算文件夾及其子文件夾中文件的總大小的函數:
def get_tree_size(path):
total = 0
with os.scandir(path) as entries:
for entry in entries:
if entry.is_dir(follow_symlinks=False):
total += get_tree_size(entry.path)
else:
total += entry.stat(follow_symlinks=False).st_size
return total
在Go中(一旦1.16發佈),對應的函數以下所示:
func GetTreeSize(path string) (int64, error) {
entries, err := os.ReadDir(path)
if err != nil {
return 0, err
}
var total int64
for _, entry := range entries {
if entry.IsDir() {
size, err := GetTreeSize(filepath.Join(path, entry.Name()))
if err != nil {
return 0, err
}
total += size
} else {
info, err := entry.Info()
if err != nil {
return 0, err
}
total += info.Size()
}
}
return total, nil
}
高級結構很類似,固然有人可能會說:「看,Go的錯誤處理多麼繁瑣!」沒錯——Python代碼很是簡潔。在簡短腳本的狀況下這沒有問題,而這也是Python的優點。
然而,在生產環境的代碼中,或者在一個頻繁使用的命令行工具庫中,捕獲stat調用的錯誤會更好,進而能夠忽略權限錯誤或者記錄日誌。Go代碼能夠明確看到錯誤發生的狀況,可讓你輕鬆添加日誌或者打印的錯誤信息更好。
更高級的目錄樹遍歷
另外,兩個語言都有更高級的循環遍歷目錄的函數。在Python中,它是os.walk
。Python中scandir
的美妙之處在於os.walk
的簽名無需改變,所以全部os.walk
的用戶(有很是多)均可以自動獲得加速。
好比,使用os.walk
打印文件夾下全部非點的路徑:
def list_non_dot(path):
paths = []
for root, dirs, files in os.walk(path):
# Modify dirs to skip directories starting with '.'
dirs[:] = [d for d in dirs if not d.startswith('.')]
for f in files:
if f.startswith('.'):
continue
paths.append(os.path.join(root, f))
return sorted(paths)
從Python3.5開始,os.walk
底層使用scandir
代替listdir
,根據操做系統和文件系統,這能夠顯著提高1.5到20倍的速度。
Go (pre-1.16版本)語言中有一個類似的函數,filepath.Walk
,但不幸的是 FileInfo
接口的設計沒法支持各類方法調用時的錯誤報告。正如咱們所知,有時函數會進行系統調用——好比,像Size
這樣的統計信息在Linux下老是須要一次系統調用。所以在Go語言中,這些方法須要返回錯誤(在Python中它們會拋出異常)。
是否要嘗試去掉錯誤處理的邏輯來重複使用 FileInfo
接口,這樣現有代碼就能夠顯著提速。實際上,Russ Cox提出一個提案 issue 41188就是這個思路(提供了一些數據來代表這個想法並不像聽起來那麼不靠譜)。然而,stat
確實會返回錯誤,所以像文件大小這樣潛在的屬性應該在錯誤時返回0。這樣對應的結果是,要把這個邏輯嵌入到現有的API中,須要大量須要推進改動的地方,最後Russ確認 沒法就此達成共識,並提出 DirEntry
接口。
這代表,爲了得到性能提高, filepath.Walk
的調用須要改爲 filepath.WalkDir
——儘管很是類似,但遍歷函數的參數是DirEntry
而不是 FileInfo
。
下面的代碼是Go版本的使用現有filepath.Walk
函數的list_non_dot
:
func ListNonDot(path string) ([]string, error) {
var paths []string
err := filepath.Walk(path, func(p string, info os.FileInfo,
err error) error {
if strings.HasPrefix(info.Name(), ".") {
if info.IsDir() {
return filepath.SkipDir
}
return err
}
if !info.IsDir() {
paths = append(paths, p)
}
return err
})
return paths, err
}
固然,在Go 1.16中這段代碼也能夠運行,但若是你想獲得性能收益就須要作少量修改——在上面的代碼中僅須要把 Walk
替換爲 WalkDir
,並把 os.FileInfo
替換成 os.DirEntry
:
err := filepath.WalkDir(path, func(p string, info os.DirEntry,
對於這麼修改的價值,在個人Linux home文件夾下運行第一個函數,在緩存後花費約580ms。使用Go 1.16中的新版本花費約370ms——差很少快了1.5倍。差別並不大,但也是有意義的——在網絡文件系統和Windows下將會獲得更多的加速效果。
總結
新的ReadDir
API易於使用,經過 fs.ReadDir
能夠便捷地集成新的文件系統。相比於加速現有的Walk
調用,你所須要替換成WalkDir
的改動微不足道。
API 的設計很是難。跨平臺、操做系統相關的API設計更加困難。但願你在設計下一個編程語言的標準庫時能夠設計正確!:-)
不管如何,我很開心能夠看到Go對於文件夾讀取的支持將不在落後——或者說_努力_緊追——Python。
本文分享自微信公衆號 - GoCN(golangchina)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。