登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

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 与名称重新关联,不能混用两个参照点。

文件系统根子树与相对路径清单的静态关系
静态结构说明图:root 决定 sub 的根,WalkDir 在 sub 上取得相对名称,names 不含被选中的根目录前缀。

路径使用 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 判断底层错误。

遍历回调参数与结果错误接口的静态关系
静态结构说明图:先处理 walkErr 再访问 DirEntry;错误由包装链保留,成功的空 names 与失败返回分开。

函数选择“失败不交付清单”:即便前面已经收集到一些名称,后续访问失败也返回 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。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>