• Golang 递归获取目录下所有文件


    1.问题

    如果我想获取一个目录下的所有文件列表,使用 Golang 该如何实现呢?

    比如有个目录 dir 结构如下:

    tree dir
    dir
    ├── bar.txt
    ├── foo.txt
    └── subdir
        └── baz.txt
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    那么如何获取 dir 目录下的所有文件路径呢?

    dir/foo.txt
    dir/bar.txt
    dir/subdir/baz.txt
    
    • 1
    • 2
    • 3

    2.io/ioutil

    标准库 io/ioutil 包提供了一个函数 ReadDir() 可以获取指定目录下的所有内容,按文件名排序,返回 []fs.FileInfo 切片来描述目录中的所有内容。

    func ReadDir(dirname string) ([]fs.FileInfo, error)
    
    • 1

    利用 ioutil.ReadDir() 我们可以获取目录中的所有文件吗?

    // ListDir lists all the file or dir names in the specified directory.
    // Note that ListDir don't traverse recursively.
    func ListDir(dirname string) ([]string, error) {
    	infos, err := ioutil.ReadDir(dirname)
    	if err != nil {
    		return nil, err
    	}
    	names := make([]string, len(infos))
    	for i, info := range infos {
    		names[i] = info.Name()
    	}
    	return names, nil
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    我们来测试一下:

    package main
    
    import (
    	"fmt"
    	"io/ioutil"
    )
    
    func main() {
    	names, _ := ListDir("dir")
    	fmt.Printf("names:%v\n", names)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    运行输出:

    names:[bar.txt foo.txt subdir]
    
    • 1

    可见 ioutil.ReadDir() 并不会递归获取子目录的内容。

    3.递归获取

    如果想递归获子目录的内容,该如何实现呢?

    我们可以递归调用我们自己的函数,来递归遍历子目录。

    // GetDirAllFilePaths gets all the file paths in the specified directory recursively.
    func GetDirAllFilePaths(dirname string) ([]string, error) {
    	// Remove the trailing path separator if dirname has.
    	dirname = strings.TrimSuffix(dirname, string(os.PathSeparator))
    
    	infos, err := ioutil.ReadDir(dirname)
    	if err != nil {
    		return nil, err
    	}
    
    	paths := make([]string, 0, len(infos))
    	for _, info := range infos {
    		path := dirname + string(os.PathSeparator) + info.Name()
    		if info.IsDir() {
    			tmp, err := GetDirAllFilePaths(path)
    			if err != nil {
    				return nil, err
    			}
    			paths = append(paths, tmp...)
    			continue
    		}
    		paths = append(paths, path)
    	}
    	return paths, nil
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    我们来测试一下:

    package main
    
    import (
    	"fmt"
    	"io/ioutil"
    	"os"
    	"strings"
    )
    
    func main() {
    	paths, _ := GetDirAllFilePaths("dir/")
    	for _, path := range paths {
    		fmt.Println(path)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    运行输出:

    dir/bar.txt
    dir/foo.txt
    dir/subdir/baz.txt
    
    • 1
    • 2
    • 3

    哇,看起来大功告成。但果真如此吗?

    4.包含符号链接的情况

    如果我们此时在目录 dir 中加入一个符号链接,指向另外一个目录,那结果会如何呢?

    tree dir
    dir
    ├── bar.txt
    ├── foo.txt
    ├── subdir
    │   └── baz.txt
    └── zipln -> ../zip
    
    tree zip
    zip
    └── qux.txt
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    还是运行调用 GetDirAllFilePaths(),我们得到的结果如下:

    dir/bar.txt
    dir/foo.txt
    dir/subdir/baz.txt
    dir/zipln
    
    • 1
    • 2
    • 3
    • 4

    可见,当子目录为符号链接时,我们并没有访问链接指向的目标文件。

    我们改变一下实现,当子目录是符号链接时,读取目标目录下的文件。

    // GetDirAllFilePathsFollowSymlink gets all the file paths in the specified directory recursively.
    func GetDirAllFilePathsFollowSymlink(dirname string) ([]string, error) {
    	// Remove the trailing path separator if dirname has.
    	dirname = strings.TrimSuffix(dirname, string(os.PathSeparator))
    
    	infos, err := ioutil.ReadDir(dirname)
    	if err != nil {
    		return nil, err
    	}
    
    	paths := make([]string, 0, len(infos))
    	for _, info := range infos {
    		path := dirname + string(os.PathSeparator) + info.Name()
    		realInfo, err := os.Stat(path)
    		if err != nil {
    			return nil, err
    		}
    		if realInfo.IsDir() {
    			tmp, err := GetDirAllFilePathFollowSymlink(path)
    			if err != nil {
    				return nil, err
    			}
    			paths = append(paths, tmp...)
    			continue
    		}
    		paths = append(paths, path)
    	}
    	return paths, nil
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    我们来测试一下:

    package main
    
    import (
    	"fmt"
    	"io/ioutil"
    	"os"
    	"strings"
    )
    
    func main() {
    	paths, _ := GetDirAllFilePathsFollowSymlink("dir/")
    	for _, path := range paths {
    		fmt.Println(path)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    运行输出:

    dir/bar.txt
    dir/foo.txt
    dir/subdir/baz.txt
    dir/zipln/qux.txt
    
    • 1
    • 2
    • 3
    • 4

    perfect,这就是我们想要的效果。

    5.同时返回目录的路径

    有时,我们还需要目录路径,即获取指定目录下的文件和子目录的路径。比如在对一个目录进行压缩时会需要。

    还是以上文 dir 目录为例,我们想要的结果是:

    dir
    dir/bar.txt
    dir/foo.txt
    dir/subdir
    dir/subdir/baz.txt
    dir/zipln
    dir/zipln/qux.txt
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    我们只要稍微改造 GetDirAllFilePathsGetDirAllFilePathsFollowSymlink 即可,在遍历时把当前的目录加入结果集。

    并更名 GetDirAllFilePathsGetDirAllEntryPathsGetDirAllFilePathsFollowSymlinkGetDirAllEntryPathsFollowSymlink,因为条目(Entry)比文件(File)语义更符合函数的功能,因为不仅可以获取文件,也可以获取目录的路径。

    // GetDirAllEntryPaths gets all the file or dir paths in the specified directory recursively.
    // Note that GetDirAllEntryPaths won't follow symlink if the subdir is a symbolic link.
    func GetDirAllEntryPaths(dirname string, incl bool) ([]string, error) {
    	// Remove the trailing path separator if dirname has.
    	dirname = strings.TrimSuffix(dirname, string(os.PathSeparator))
    
    	infos, err := ioutil.ReadDir(dirname)
    	if err != nil {
    		return nil, err
    	}
    
    	paths := make([]string, 0, len(infos))
    	// Include current dir.
    	if incl {
    		paths = append(paths, dirname)
    	}
    
    	for _, info := range infos {
    		path := dirname + string(os.PathSeparator) + info.Name()
    		if info.IsDir() {
    			tmp, err := GetDirAllEntryPaths(path, incl)
    			if err != nil {
    				return nil, err
    			}
    			paths = append(paths, tmp...)
    			continue
    		}
    		paths = append(paths, path)
    	}
    	return paths, nil
    }
    
    // GetDirAllEntryPathsFollowSymlink gets all the file or dir paths in the specified directory recursively.
    func GetDirAllEntryPathsFollowSymlink(dirname string, incl bool) ([]string, error) {
    	// Remove the trailing path separator if dirname has.
    	dirname = strings.TrimSuffix(dirname, string(os.PathSeparator))
    
    	infos, err := ioutil.ReadDir(dirname)
    	if err != nil {
    		return nil, err
    	}
    
    	paths := make([]string, 0, len(infos))
    	// Include current dir.
    	if incl {
    		paths = append(paths, dirname)
    	}
    
    	for _, info := range infos {
    		path := dirname + string(os.PathSeparator) + info.Name()
    		realInfo, err := os.Stat(path)
    		if err != nil {
    			return nil, err
    		}
    		if realInfo.IsDir() {
    			tmp, err := GetDirAllEntryPathsFollowSymlink(path, incl)
    			if err != nil {
    				return nil, err
    			}
    			paths = append(paths, tmp...)
    			continue
    		}
    		paths = append(paths, path)
    	}
    	return paths, nil
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

    我们测试一下。

    func main() {
    	fmt.Println("not follow symlink:")
    	paths, _ := GetDirAllEntryPaths("dir/", true)
    	for _, path := range paths {
    		fmt.Println(path)
    	}
    
    	fmt.Println("\nfollow symlink:")
    	paths, _ = GetDirAllEntryPathsFollowSymlink("dir/", true)
    	for _, path := range paths {
    		fmt.Println(path)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    运行输出:

    not follow symlink:
    dir
    dir/bar.txt
    dir/foo.txt
    dir/subdir
    dir/subdir/baz.txt
    dir/zipln
    
    follow symlink:
    dir
    dir/bar.txt
    dir/foo.txt
    dir/subdir
    dir/subdir/baz.txt
    dir/zipln
    dir/zipln/qux.txt
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    6.dablelv/cyan

    以上函数已放置开源库 dablelv/cyan,可 import 直接使用。

    package main
    
    import (
    	"github.com/dablelv/cyan/file"
    )
    
    func main() {
    	// 获取目录下所有文件和子目录名称(不会递归)。
    	names, _ := file.ListDir("dir")
    
    	// 递归获取目录下所有文件路径(不解析符号链接)
    	paths, _ := file.GetDirAllEntryPaths("dir", false)
    	// 递归获取目录下所有文件和目录路径(不解析符号链接)
    	paths, _ = file.GetDirAllEntryPaths("dir", true)
    
    	// 递归获取目录下所有文件路径(解析符号链接)
    	paths, _ = file.GetDirAllEntryPathsFollowSymlink("dir", false)
    	// 递归获取目录下所有文件与目录路径(解析符号链接)
    	paths, _ = file.GetDirAllEntryPathsFollowSymlink("dir/", true)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    欢迎大家 Star & PR。


    参考文献

    io/ioutil - Go Packages

  • 相关阅读:
    简单工厂模式
    kube-proxy 切换为ipvs模式
    二、PostgreSQL初始化配置&启动
    章节十:Selenium
    强烈推荐 20.7k Star!企业级商城开源项目强烈推荐!基于DDD领域驱动设计模型,助您快速掌握技术奥秘,实现业务快速增长
    excel中的OFFSET函数
    Vue事件
    栈(扩容)的初始化、判满、扩容、入栈、获取栈顶元素且删除、获取栈顶元素不删除等等
    实时音视频方案汇总
    “Cloud“(云)
  • 原文地址:https://blog.csdn.net/K346K346/article/details/128024386