原文地址: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源创计划”,欢迎正在阅读的你也加入,一块儿分享。