Golang实践录:获取目录文件列表
Posted 李迟
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang实践录:获取目录文件列表相关的知识,希望对你有一定的参考价值。
获取目录下匹配某种规则的文件,返回文件列表,在开发中比较常用。本文实现此功能,并做了些扩展。
起因
笔者开发的内部工具,需要查找各式文件,比如:
- 数据文件,以csv结尾;
- 信息文件,以md结尾(因为已有工具生成并渲染得html发布到网页上,直接拿来使用);
- 压缩包,以zip结尾。
由于设计原因,部分不同各类的文件会在同一目录出现,因此,要实现一个接口用以获取某个目录下符合条件的文件。
设计思路如下:
- 为简单化,不递归查找子目录,当传入的非目录或不存在时,直接返回即可。
- 设计是否带目录标志,有时仅需文件名称,而不需要其目录。
- 设计返回文件列表数量,0表示所有文件,因此有时仅需要得到符合要求的第一个文件。
- 设计是否排序标志,因为默认是升序,所以该标志实际是降序标志。
实现
为减少篇幅,仅摘录必要的源码。
根据匹配字符串(为描述方便,称之为patten
)的位置,需实现如下接口:
- GetFileListBySuffix:patten 作为后缀,可按后缀名匹配。通过
strings.HasSuffix
判断。 - GetFileListByPrefix:同前,但是是前缀。通过
strings.HasSuffix
判断。 - GetFileListByKey:同上,在任意位置出现均可。通过
strings.HasPrefix
判断。
另外实现os.FileInfo
三个接口,达到降序排序目的。通过strings.Contains
判断。
封装
代码如下:
// 按文件名排序,可扩展至文件时间
type byName []os.FileInfo
//func (f byName) Less(i, j int) bool return f[i].Name() < f[j].Name() // 文件名升序,默认方式
func (f byName) Less(i, j int) bool return f[i].Name() > f[j].Name() // 文件名倒序
func (f byName) Len() int return len(f)
func (f byName) Swap(i, j int) f[i], f[j] = f[j], f[i]
// GetFileListBySuffix returns an ordered list of file paths.
// It recognize if given path is a file, and don't do recursive find.
func GetFileListBySuffix(dirPath, suffix string, needDir bool, isDescend bool, num int) ([]string, error)
if !IsExist(dirPath)
return nil, fmt.Errorf("given path does not exist: %s", dirPath)
else if IsFile(dirPath)
return []stringdirPath, nil
// Given path is a directory.
dir, err := os.Open(dirPath)
if err != nil
return nil, err
fis, err := dir.Readdir(0)
if err != nil
return nil, err
if isDescend
sort.Sort(byName(fis))
if num == 0
num = len(fis)
files := make([]string, 0, num)
for i := 0; i < num; i++
fi := fis[i]
if strings.HasSuffix(fi.Name(), suffix)
if needDir
files = append(files, filepath.Join(dirPath, fi.Name()))
else
files = append(files, fi.Name())
return files, nil
// as GetFileListBySuffix, but for Prefix
func GetFileListByPrefix(dirPath, suffix string, needDir bool, isDescend bool, num int) ([]string, error)
if !IsExist(dirPath)
return nil, fmt.Errorf("given path does not exist: %s", dirPath)
else if IsFile(dirPath)
return []stringdirPath, nil
// Given path is a directory.
dir, err := os.Open(dirPath)
if err != nil
return nil, err
fis, err := dir.Readdir(0)
if err != nil
return nil, err
if isDescend
sort.Sort(byName(fis))
if num == 0
num = len(fis)
files := make([]string, 0, num)
for i := 0; i < num; i++
fi := fis[i]
if strings.HasPrefix(fi.Name(), suffix)
if needDir
files = append(files, filepath.Join(dirPath, fi.Name()))
else
files = append(files, fi.Name())
return files, nil
// 根据关键字查找
func GetFileListByKey(dirPath, key string, needDir bool, isDescend bool, num int) ([]string, error)
if !IsExist(dirPath)
return nil, fmt.Errorf("given path does not exist: %s", dirPath)
else if IsFile(dirPath)
return []stringdirPath, nil
// Given path is a directory.
dir, err := os.Open(dirPath)
if err != nil
return nil, err
fis, err := dir.Readdir(0)
if err != nil
return nil, err
if isDescend
sort.Sort(byName(fis))
if num == 0
num = len(fis)
files := make([]string, 0, num)
for i := 0; i < num; i++
fi := fis[i]
if strings.Contains(fi.Name(), key)
if needDir
files = append(files, filepath.Join(dirPath, fi.Name()))
else
files = append(files, fi.Name())
return files, nil
测试及结果
测试用到的目录及文件如下:
$ tree foo/
foo/
|-- bar_1.json
|-- bar_10.json
|-- bar_2.json
|-- bar_20.json
|-- bar_3.json
|-- log.1
|-- log.10
|-- log.2
|-- log.20
`-- log.3
0 directories, 10 files
前缀匹配调用:
list, err1 := GetFileListByPrefix("./foo", "log", true, false, 0)
fmt.Println(list, err1)
结果:
[foo\\log.1 foo\\log.10 foo\\log.2 foo\\log.20 foo\\log.3] <nil>
后缀匹配调用:
list, err1 = GetFileListBySuffix("./foo", "json", true, false, 0)
fmt.Println(list, err1)
结果:
[foo\\bar_1.json foo\\bar_10.json foo\\bar_2.json foo\\bar_20.json foo\\bar_3.json] <nil>
一般匹配调用:
list, err1 = GetFileListByKey("./foo", "1", true, false, 0)
fmt.Println(list, err1)
结果:
[foo\\bar_1.json foo\\bar_10.json foo\\log.1 foo\\log.10] <nil>
从结果分析,符合预期。
扩展
优化接口
从前面封装的接口代码分析,绝大部分代码是相同的,仅仅是读取后对文件名称的判断不同而已。因此,可以将它们合成一个接口。设计如下:
patten
额外添加*
,通过*
位置匹配前缀(foo*
)、后缀(*foo
)、中间(*foo*
,即包含该关键字)。- 在内部判断并去掉
*
。其余机制同前。
代码如下:
func GetFileListByKey_new(dirPath, patten string, needDir bool, isDescend bool, num int) ([]string, error)
if !IsExist(dirPath)
return nil, fmt.Errorf("given path does not exist: %s", dirPath)
else if IsFile(dirPath) // 如果是文件,返回该文件
return []stringdirPath, nil
if len(patten) < 2
return nil, fmt.Errorf("given patten len < 2")
// 默认为中间
pos := 3
rpatten := patten
if patten[0] == '*' && patten[len(patten)-1] == '*' // 一般
rpatten = patten[1 : len(patten)-1]
pos = 3
else if patten[0] == '*' // 后缀
pos = 1
rpatten = patten[1:]
else if patten[len(patten)-1] == '*' // 前缀
rpatten = patten[:len(patten)-1]
pos = 2
// Given path is a directory.
dir, err := os.Open(dirPath)
if err != nil
return nil, err
fis, err := dir.Readdir(0)
if err != nil
return nil, err
fnum := len(fis)
if fnum == 0
return []string, nil
if isDescend
sort.Sort(byName(fis))
if num == 0
num = fnum
else
if num > fnum // 如指定数量多于实际的,则使用实际
num = fnum
files := make([]string, 0, num)
checkFile := func(filename string) bool
if pos == 1
return strings.HasSuffix(filename, rpatten)
else if pos == 2
return strings.HasPrefix(filename, rpatten)
else if pos == 3
return strings.Contains(filename, rpatten)
return true
for i := 0; i < num; i++
fi := fis[i]
if checkFile(fi.Name())
if needDir
files = append(files, filepath.Join(dirPath, fi.Name()))
else
files = append(files, fi.Name())
return files, nil
调用如下:
list, err1 = GetFileListByKey_new("./foo", "log*", true, false, 0)
fmt.Println(list, err1)
list, err1 = GetFileListByKey_new("./foo", "*json", true, false, 0)
fmt.Println(list, err1)
list, err1 = GetFileListByKey_new("./foo/", "*1*", true, false, 0)
fmt.Println(list, err1)
结果如下:
[foo\\log.1 foo\\log.10 foo\\log.2 foo\\log.20 foo\\log.3] <nil>
[foo\\bar_1.json foo\\bar_10.json foo\\bar_2.json foo\\bar_20.json foo\\bar_3.json] <nil>
[foo\\bar_1.json foo\\bar_10.json foo\\log.1 foo\\log.10] <nil>
正则表达式
虽然目前未使用到,但还是实现了按正则表达式来匹配文件名的接口,实现如下:
// 根据正规正则表达式查找
func GetFileListByPatten(dirPath, patten string, needDir bool, isDescend bool, num int) ([]string, error)
if !IsExist(dirPath)
return nil, fmt.Errorf("given path does not exist: %s", dirPath)
else if IsFile(dirPath)
return []stringdirPath, nil
// Given path is a directory.
dir, err := os.Open(dirPath)
if err != nil
return nil, err
fis, err := dir.Readdir(0)
if err != nil
return nil, err
fnum := len(fis)
if fnum == 0
return []string, nil
if isDescend
sort.Sort(byName(fis))
if num == 0
num = fnum
else
if num > fnum // 如指定数量多于实际的,则使用实际
num = fnum
match := func(str string, patten string) bool
rep := regexp.MustCompile(patten)
ret := rep.MatchString(str)
return ret
files := make([]string, 0, num)
for i := 0; i < num; i++
fi := fis[i]
if match(fi.Name(), patten)
if needDir
files = append(files, filepath.Join(dirPath, fi.Name()))
else
files = append(files, fi.Name())
return files, nil
调用如下:
list, err1 = GetFileListByPatten("./foo", `log.+`, true, false, 0)
fmt.Println(`log.+ : `, list, err1)
list, err1 = GetFileListByPatten("./foo/", `[a-z0-9_]*.json`, true, false, 0)
fmt.Println(`[a-z0-9_]*.json : `, list, err1)
结果如下:
log.+ : [foo\\log.1 foo\\log.10 foo\\log.2 foo\\log.20 foo\\log.3] <nil>
[a-z0-9_]*.json : [foo\\bar_1.json foo\\bar_10.json foo\\bar_2.json foo\\bar_20.json foo\\bar_3.json] <nil>
说明:正则表达式一定要符合要求,否则不识别。
带数字的文件名称排序
前面的测试结果:
[foo\\log.1 foo\\log.10 foo\\log.2 foo\\log.20 foo\\log.3]
可以看到,文件的排序不是我们想象中那样的顺序,实际上,从字符串比较(因为代码就是使用这个方式)的角度看,上述排序是正常的。如要达到想象的顺序,则要修改比较方式,重新实现os.FileInfo
中的Less
接口。
因为带数字的文件名称有各式各样,此处假定按文件的“后缀名”数字由大到小排序。实际上,这是真实的的场合:某工程程序产生的日志,按超过指定大小会自动创建新的文件,后缀带数字即表达有多少个日志文件。
代码如下:
// 按带数字的文件名排序
type byNumericalFilename []os.FileInfo
func (f byNumericalFilename) Len() int return len(f)
func (f byNumericalFilename) Swap(i, j int) f[i], f[j] = f[j], f[i]
func (f byNumericalFilename) Less(i, j int) bool
pathA := f[i].Name()
pathB := f[j].Name()
// !! 根据需求,文件最后是数字,按其值降序排序,示例:ddd.log.x.1 ddd.log.x.2
// 如有其它者,也可以修改
a, err1 := strconv.Atoi(pathA[strings.LastIndex(pathA, ".")+1:])
b, err2 := strconv.Atoi(pathB[strings.LastIndex(pathB, ".")+1:])
// 整体文件(不含后缀名)名称排序,名称是数字,如1.txt 2.txt 10.txt
// a, err1 := strconv.Atoi(pathA[0:strings.LastIndex(pathA, ".")])
// b, err2 := strconv.Atoi(pathB[0:strings.LastIndex(pathB, ".")])
// 有错误,默认降序
if err1 != nil || err2 != nil
return pathA > pathB
// 按数字降序
return a > b
func getFileListByPrefix(dirPath, suffix string, needDir bool, num int) ([]string, error)
if !isExist(dirPath)
return nil, fmt.Errorf("given path does not exist: %s", dirPath)
else if isFile(dirPath)
return []stringdirPath, nil
// Given path is a directory.
dir, err := os.Open(dirPath)
if err != nil
return nil, err
fis, err := dir.Readdir(0)
if err != nil
return nil, err
fnum := len(fis)
if fnum == 0
return []string, nil
sort.Sort(byNumericalFilename(fis))
if num == 0
num = fnum
else
if num > fnum
num = fnum
files := make([]string, 0, num)
for i := 0; i < num; i++
fi := fis[i]
if strings.HasPrefix(fi.Name(), suffix)
if needDir
files = append(files, filepath.Join(dirPath, fi.Name()))
else
files = append(files, fi.Name())
return files, nil
调用如下:
list, err1 = getFileListByPrefix("./foo/", `log`, true, 0)
fmt.Println(`sort number : `, list, err1)
结果如下:
sort number : [foo\\log.20 foo\\log.10 foo\\log.3 foo\\log.2 foo\\log.1] <nil>
从结果看,已实现根据数字从大到小排序。
总结
文中所实现的接口,已应用到笔者实际的工程。目前未发现有什么问题。
隔了一天的PS:
由于传递参数可以指定文件数量,当指定的数量多了实际数量时,会报错,需要加上判断。在“优化接口”及后面小节的代码中已修正,前面由于实际中不使用,故保留。
有时话真的不是说得太满。记之并以此为鉴。
李迟 2022.10.22 10.24
以上是关于Golang实践录:获取目录文件列表的主要内容,如果未能解决你的问题,请参考以下文章