您好,登錄后才能下訂單哦!
這期內容當中小編將會給大家帶來有關Go語言中ReadDir 和 DirEntry的區別是什么,文章內容豐富且以專業的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
簡短的答案是:性能。
當調用讀取文件夾路徑的系統函數時,操作系統一般會返回文件名_和_它的類型(在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和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,
上述就是小編為大家分享的Go語言中ReadDir 和 DirEntry的區別是什么了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。