目录遍历是一个很常见的操作,它的使用场景有如文件目录查看(最典型的应用如 ls 命令)、文件系统清理、日志分析、项目构建等。
本文将尝试逐步介绍在 go 中几种遍历目录文件的方法,从传统的 ioutil.readdir 函数开始,逐渐深入。
文中也会提供示例代码、提供一些性能剖析,以便于大家更好地理解。
ioutil.readdir
首先,go 中目录文件遍历的第一种方式是 ioutil.readdir
函数。
在 go 1.16 版本前,ioutil.readdir
就是遍历目录的标准方法,它的返回结构是目录中文件的 fileinfo
列表,简单直接。
示例代码:
func main() { files, err := ioutil.readdir(".") if err != nil { log.fatal(err) } for _, f := range files { fmt.println(f.name()) } }
但它的缺点也非常明显,性能不高。导致它的主要原因有如下几点:
完全加载
这就导致了 ioutil.readdir
在返回结果前,会将目录下所有文件的信息完全加载到内存中。对于包含大量文件的目录,它就需要在内存中存储大量的 fileinfo 对象,毫无疑问,这会增加内存使用。
fileinfo 开销
由于是完全加载,每个 fileinfo 对象都包含了文件的详细信息,如文件名、大小、修改时间等都会在返回之前都已经加载完成。但获取这些信息需进行系统调用。而每个文件都要做这样的调用,当文件数量很多时,这些系统调用的累积开销可以变得不容忽视了。
无法分批处理
由于 ioutil.readdir
是一次性返回所有文件信息,没有提供分批处理的能力。无论目录中有多少文件,都要等待所有文件信息读取完成,这在处理目录中包含大量文件的场景中,也就无法提前并行处理,效率是可想而知的。
这一点其实和我们前面的一篇文章,介绍的 go 中按行(或者说按块)读取文件的逻辑是类似的,一次加载全部内容,有潜在的性能问题。
由于 ioutil.readdir
有这么多的缺点,所以它在 go 1.16 及更高版本已经被弃用了。
那现在我们该用什么方法呢?
os.readdir
从 go 1.16 版本起,标准库针对目录遍历查看提供了新的函数 os.readdir
,以用来简化和提高遍历目录文件的效率。
函数签名如下:
func readdir(name string) ([]direntry, error)
os.readdir
函数返回一个按文件名排序的 direntry
类型切片。如果在读取目录项时遇到错误,它也会尽量返回已读取内容。这种设计同时兼顾了效率和错误处理的需要。
示例代码:
func main() { files, err := os.readdir(".") if err != nil { log.fatal(err) } for _, file := range files { fmt.println(file.name()) } }
os.readdir
相比于旧方法 ioutil.readdir
的有什么优势?为什么丢弃 ioutil.readdir
而引入这个新的 os.readdir
。
如果对比两者源码,会发现差异主要在返回的类型上。os.readdir
返回的 []direntry
而非 []fileinfo
。它还具有性能优势。
为什么?
因为 direntry
允许按需获取文件详情,即懒加载,而非是遍历目录时立即加载所有文件属性。很多场景下,我们并不需要
我在 macos 系统下测试的 direntry
接口的实际变量类型为 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) }
我们只有在调用 info
方法时,才会真正通过 lstat
发起系统调用。
如果你有将旧代码迁移到 direntry
的需求, go 1.17 还引入了 fs.fileinfotodirentry
函数,允许我们将 fileinfo
对象转换为 direntry
对象。
info, _ := os.stat("somefile") direntry := fs.fileinfotodirentry(info)
看到这,对于认真思考的朋友,或许已经发现我们还有一个问题没解决,即 os.readdir
不是也不支持分批处理的能力吗?
继续往下看吧,我将介绍一个更底层的方法。
os.file 的 readdir 方法
我们知道 os.open
是用于打开文件的,但其实它也可用于打开目录。如果 os.open
打开的是目录,我们在它返回的 os.file
上调用 readdir
以查看目录内容。
示例代码:
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()) } }
如上的代码其实类似于 os.readdir
内容的实现代码。
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 }
这种方法更底层,提供了更多的灵活性。我们就可以用它分批读取目标。
如何实现呢?
核心就是那句的 dir.readdir(-1)
,它的入参指定了每次读取文件的数量,而 -1
表示读取目录的所有内容。我们只要将 -1 改为分批读取的数量即可,多次循环即可。
示例代码:
func main() { dir, err := os.open(".") if err != nil { log.fatal(err) } defer dir.close() for { files, err := dir.readdir(10) // 每批读取10个条目 if err == io.eof { break // 遍历完成 } if err != nil { log.fatal(err) // 处理其他错误 } for _, file := range files { fmt.println(file.name()) } } }
这段代码演示了如何使用 file.readdir
分批处理目录中的文件。通过这种方式,可以更有效地管理内存使用。
补充一点
在写这篇文章时,我发现 os.file
有两个查看目录的方法,分别是 readdir
和 readdir
。功能上的区别是新的 readdir
返回的是 []direntry
,而 readdir
返回的是 []fileinfo
。
换句话说,readdir 本质上是 readdir 的升级版。
它们的函数签名,如下所示:
func (f *file) readdir(n int) ([]fileinfo, error) func (f *file) readdir(n int) ([]direntry, error)
这算是不支持可选参数和重载,但要解决兼容问题采取的措施吗?真的是蚌埠住了。
目录的递归遍历
现在,还差最后一个内容没有介绍,那就是递归目录遍历。
针对目录的递归遍历,go 中提供了一个专门的函数,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) } }
我们通过遍历的回调函数中在处理每个文件。它简化了目录的递归遍历,但对于大型或深层次的目录结构,同样存在着提前加载 fileinfo 的问题。
针对这个问题,在 go1.16 版本也引入了基于 direntry
版的 filepath.walkdir
函数。
filepath.walkdir
的函数签名如下:
func walkdir(root string, fn fs.walkdirfunc) error
fs.walkdirfunc
的定义如下:
type walkdirfunc func(path string, d direntry, err error) error
新函数的遍历回调参数是 direntry
,而非 fileinfo
。现在,filepath.walkdir
也有了延迟加载 fileinfo
的能力了。
现在,我们再来看下这张图。
总结
在本文中,我们系统介绍了 go 中多种遍历目录文件的方法。从传统的 ioutil.readdir
,到 go 1.16 引入的 os.readdir
,os.file
的 readdir
方法。每种方法适用于不同的场景,如何选择要取决于你的需求、go 版本、性能。如果你需要递归遍历,也可以使用基于 direntry
的 filepath.walkdir
实现,提高遍历的性能。
以上就是详解go语言中如何高效遍历目录的详细内容,更多关于go遍历目录的资料请关注代码网其它相关文章!
发表评论