Go io/fs 怎样遍历文件系统并保留相对路径处理空输入并保留错误边界
来源:17golang原创
时间:2026-11-07 02:05:58 138浏览 收藏
用 io/fs.WalkDir 遍历时,要先确定路径相对于谁。如果希望清单相对于某个子目录,可先用 fs.Sub 选择子树,再从 "." 开始遍历,直接保留回调里的路径。空输入如何解释、空目录如何返回、出错后是否交付部分清单,都应由封装函数明确规定。
Go 官方地址:https://go.dev/
为什么清单里多了一段目录名
一个常见现象是希望拿到 docs/intro.txt,实际却拿到 assets/docs/intro.txt。先看传入的文件系统和 root,而不是马上删除字符串前缀。回调名称包含 root 的路径部分;DirEntry.Name() 又只有最后一段名称,换成它会丢失子目录。
本例约定:只收集普通文件,返回相对指定目录的名称,失败时不返回部分结果。它适合生成资源索引,不负责读取文件内容或处理磁盘写入。
把相对路径的参照点固定下来
把原文件系统记作 fsys,用户选中的目录记作 root,使用 fs.Sub 生成 sub。然后遍历 sub 的根。这样 names 中的名称可以直接作为 sub 的读取路径;若还要从原 fsys 读取,则需要把原 root 与名称重新关联,不能混用两个参照点。

路径使用 io/fs 的斜杠语义,不用 filepath 的系统分隔符去解释文件系统内部名称。空字符串不是通用有效根,本例在函数入口明确将它解释为 ".";这属于业务约定。其他非法名称直接拒绝,不先 Clean 或 TrimSpace,以免静默改变调用方意图。
用完整函数区分空输入和无文件
下面使用 fstest.MapFS 提供示例资源,因此无需先创建磁盘目录。MapFS 在这里是演示载体;实际使用时可以传入 embed.FS 或适合业务的其他实现。代码先检查根确实是目录,再生成完整清单。
package main
import (
"errors"
"fmt"
"io/fs"
"os"
"testing/fstest"
)
// ListRelative 返回相对 root 的普通文件名;空字符串按本函数约定表示 "."。
func ListRelative(fsys fs.FS, root string) ([]string, error) {
if fsys == nil {
return nil, errors.New("文件系统不能为空")
}
if root == "" {
root = "."
}
// 不先 Clean,避免把非法输入悄悄变成另一个路径。
if !fs.ValidPath(root) {
return nil, fmt.Errorf("根路径 %q: %w", root, fs.ErrInvalid)
}
sub, err := fs.Sub(fsys, root)
if err != nil {
return nil, fmt.Errorf("选择子树 %q: %w", root, err)
}
info, err := fs.Stat(sub, ".")
if err != nil {
return nil, fmt.Errorf("读取根 %q: %w", root, err)
}
if !info.IsDir() {
return nil, fmt.Errorf("根 %q 不是目录: %w", root, fs.ErrInvalid)
}
// 成功且无普通文件时,返回非 nil 的空切片。
names := make([]string, 0)
err = fs.WalkDir(sub, ".", func(name string, d fs.DirEntry, walkErr error) error {
// 根无法访问时 d 可能为 nil,必须先处理错误。
if walkErr != nil {
return fmt.Errorf("访问 %q: %w", name, walkErr)
}
if d.Type().IsRegular() {
names = append(names, name)
}
return nil
})
if err != nil {
// 不把半份清单作为完整结果交给调用方。
return nil, fmt.Errorf("遍历 %q: %w", root, err)
}
return names, nil
}
func run() error {
// MapFS 仅作可重复的示例数据,不读取或修改真实磁盘。
demo := fstest.MapFS{
"assets/app.toml": &fstest.MapFile{Data: []byte("mode=demo")},
"assets/docs/intro.txt": &fstest.MapFile{Data: []byte("intro")},
"empty": &fstest.MapFile{Mode: fs.ModeDir | 0755},
}
names, err := ListRelative(demo, "assets")
if err != nil {
return err
}
fmt.Printf("assets: %q\n", names)
empty, err := ListRelative(demo, "empty")
if err != nil {
return err
}
fmt.Printf("empty: count=%d nil=%t\n", len(empty), empty == nil)
// 用错误链识别不存在;不要匹配错误文本。
_, err = ListRelative(demo, "missing")
fmt.Printf("missing: isNotExist=%t\n", errors.Is(err, fs.ErrNotExist))
return nil
}
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
保存为 main.go 后,可以在独立目录运行:
# 仅在尚未初始化模块的示例目录执行一次。 go mod init example.com/relativewalk # 执行正文中的 MapFS 示例,不扫描真实目录。 go run .
按示例数据,assets 应得到 app.toml 与 docs/intro.txt 两项,empty 应得到长度为0且非 nil 的切片,missing 应被识别为不存在。这些是代码的预期结果,供读者复现时对照,并非实际运行日志。
把回调错误与空清单分开
回调里的 walkErr 必须先处理,再访问 DirEntry:根访问失败时,目录项可能没有可用值。若忽略 walkErr 并返回 nil,就可能把访问失败变成看似正常的空结果。代码用 %w 加上访问位置,入口仍可通过 errors.Is 判断底层错误。

函数选择“失败不交付清单”:即便前面已经收集到一些名称,后续访问失败也返回 nil 和错误。若业务需要部分结果,应另设结果结构记录完整性和失败项目,不能仅靠切片是否为空表达状态。
| 情况 | 本例处理 |
|---|---|
| root 为空字符串 | 按约定使用文件系统根 "." |
| 文件系统本身为 nil | 返回输入错误 |
| root 是空目录 | 空切片与 nil 错误 |
| root 不存在 | 返回保留原错误链的错误 |
| root 指向普通文件 | 按本函数目录契约拒绝 |
| 中途访问目录失败 | 停止并丢弃部分清单 |
nil 接口检查不能识别所有“装着 typed nil 的接口”场景,调用方仍须传入可用的文件系统实现。本例不借助反射猜测自定义实现状态。
对照几个容易误判的输入
建议分别调用 ListRelative(demo, "")、ListRelative(demo, "assets") 和 ListRelative(demo, "empty")。前者以整个 demo 为参照,会保留 assets 前缀;第二个只以 assets 为参照;第三个目录存在,但没有普通文件。
再用 "assets/../empty" 观察输入拒绝,用 "missing" 观察不存在的错误链。两者都没有可交付清单,但原因不同。将用户输入先规范成另一路径,会掩盖这种差异。
代码在内存中保存全部名称,文件数量很大时需考虑结果切片占用。若只需逐个处理,可以把收集动作替换为业务回调,并重新定义失败时已有副作用如何处理。本例不把扫描结果当作文件系统快照;遍历期间外部内容变化仍要另行考虑。
常见问题
为什么不直接用 Name()?它只能给出末段名称。不同目录里的同名文件会失去区分,清单应保留回调路径。
fs.Sub 成功就证明目录存在吗?不能这样假定,所以示例另行 Stat 并判断目录类型。
符号链接会列出吗?本例筛选 Type().IsRegular(),不会把符号链接当普通文件收集。该筛选只控制清单类型,不构成对任意不可信文件系统的安全隔离。
需要跳过某个目录怎么办?可在确认无回调错误后,为目标目录明确返回 fs.SkipDir;不要在普通文件分支随意使用它,否则可能跳过其父目录后续内容。这里保持完整枚举,不额外添加过滤规则。
官方参考:WalkDir 与回调契约;Sub;ValidPath。
-
860 收藏
-
843 收藏
-
826 收藏
-
809 收藏
-
792 收藏
-
481 收藏
-
307 收藏
-
338 收藏
-
444 收藏
-
237 收藏
-
453 收藏
-
141 收藏
-
113 收藏
-
414 收藏
-
477 收藏
-
476 收藏
-
176 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习