Go 1.16 即將到來的函數:ReadDir 和 DirEntry

原文地址: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: ...

訪問namepath屬性將不會拋出異常,但根據操做系統和文件系統,以及路徑是否爲符號連接,方法的調用可能會拋出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源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索