embed.FS 不支持 filepath.WalkDir,需用 fs.ReadDir 递归遍历;路径拼接用 filepath.Join;fs.Glob 可用于简单通配匹配,但受限于嵌入路径前缀和通配规则。

embed.FS 不能直接用 filepath.WalkDir 遍历
因为 embed.FS 是只读、编译期固定的虚拟文件系统,不支持 os.DirFS 那种路径遍历语义——filepath.WalkDir 底层依赖真实目录结构和 os.ReadDir 系统调用,而 embed.FS 没有“目录”概念,只有扁平化的文件名路径。你传一个 "." 或 "/" 给它,会直接 panic:panic: embed.FS does not support walking root。
正确做法是:先用 fs.ReadDir 获取根目录(即空字符串 "")下的所有条目,再对每个子项递归调用 fs.ReadDir ——注意,不是 os.ReadDir,必须用 fs.ReadDir,它接受 fs.FS 接口,兼容 embed.FS。
-
fs.ReadDir(fsys, "")返回顶层所有文件/目录条目(fs.DirEntry切片) - 对每个
entry,若entry.IsDir()为 true,则再次调用fs.ReadDir(fsys, entry.Name()) - 路径拼接必须用
filepath.Join,不能字符串拼接(Windows 路径分隔符问题) - 嵌入式文件系统没有符号链接、权限、修改时间等概念,
entry.Type()只返回os.ModeDir或0(普通文件)
如何安全递归遍历 embed.FS 的所有文件
手动实现两层或有限深度遍历最稳妥,避免无限递归(虽然 embed.FS 本身不可能有循环链接,但逻辑写错仍可能栈溢出)。以下是一个深度优先、带错误忽略的遍历示例:
func walkEmbedFS(fsys fs.FS, root string, fn func(path string, isDir bool) error) error {
entries, err := fs.ReadDir(fsys, root)
if err != nil {
return err // embed.FS 不会返回 os.ErrPermission,出错基本是路径不存在
}
for _, e := range entries {
path := filepath.Join(root, e.Name())
if e.IsDir() {
if err := fn(path, true); err != nil {
return err
}
if err := walkEmbedFS(fsys, path, fn); err != nil {
return err
}
} else {
if err := fn(path, false); err != nil {
return err
}
}
}
return nil
}
调用方式:
立即学习“go语言免费学习笔记(深入)”;
//go:embed assets/*
var assets embed.FS
<p>err := walkEmbedFS(assets, "", func(path string, isDir bool) error {
if !isDir {
fmt.Println("file:", path)
}
return nil
})
- 该函数不依赖任何 OS 调用,纯接口驱动,可同时用于
embed.FS、os.DirFS、甚至自定义内存 FS - 返回非 nil error 会立即中断整个遍历,适合做条件终止(如找到第一个匹配文件就退出)
- 若想跳过某些路径(如
assets/.gitignore),在fn回调里判断并直接 return nil 即可,无需特殊跳过机制
fs.Glob 在 embed.FS 中能用,但有路径限制
fs.Glob 是少数几个能直接作用于 embed.FS 的通用函数,但它只支持简单通配符(* 和 **),且要求模式中不能出现 .. 或以 / 开头的绝对路径——所有路径都相对于 FS 根(即空字符串 "")解析。
Colly 是一个用于 Go 语言的快速开源爬取和爬虫框架。它适用于从简单的页面提取到异步爬虫处理大量页面集合,支持请求回调和结构化解析。
常见有效写法:
-
fs.Glob(assets, "*.txt")→ 匹配根目录下所有 .txt 文件 -
fs.Glob(assets, "config/*.yaml")→ 匹配 config/ 子目录下所有 .yaml 文件 -
fs.Glob(assets, "**/*.go")→ 匹配所有嵌套路径下的 .go 文件(**表示任意深度)
无效写法:
-
fs.Glob(assets, "/assets/*.json")→ 错误:开头的/会被忽略,实际查的是assets/*.json(如果 assets/ 是嵌入路径前缀) -
fs.Glob(assets, "../*.log")→ 错误:不允许向上越界 -
fs.Glob(assets, "templates/**/index.html")→ 有效,但需确保templates/确实被嵌入
注意:fs.Glob 返回的是路径字符串切片,不提供 fs.DirEntry 元信息,如需类型判断(是否为目录),还得额外调用 fs.Stat 或 fs.ReadFile(后者对目录会返回 fs.ErrInvalid)。
embed.FS 遍历时最容易被忽略的路径边界
嵌入路径由 //go:embed 指令决定,不是当前工作目录。比如你在 cmd/app/main.go 写了:
//go:embed templates/* static/css/* var webFS embed.FS
那么 webFS 的“根”就是 templates/ 和 static/css/ 这两个前缀路径的并集,没有 templates/../static 这种跨前缀访问——它们是彼此隔离的。这意味着:
-
fs.ReadDir(webFS, "")返回的是templates/和static/css/两个条目(都是目录) -
fs.ReadDir(webFS, "templates")才能拿到templates/下的实际文件 - 如果你嵌入的是
templates/**,那templates/api/v1/就是合法子路径;但若只写了templates/*.html,那templates/api/目录根本不会存在 - 嵌入路径末尾的
/很关键://go:embed assets和//go:embed assets/效果不同——前者嵌入assets文件(如果存在),后者才嵌入assets/目录下所有内容
运行时无法动态探测嵌入了哪些路径,只能靠构建阶段确认。建议在初始化时用 fs.Glob(fsys, "**") 打印全部路径,快速验证嵌入是否符合预期。

















