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

Go Doublestar 双星号嵌在普通文件名中为什么不是递归匹配

来源:17golang原创

时间:2026-10-29 14:00:15 408浏览 收藏

Go Doublestar 中,docs/**.md 的双星号和扩展名位于同一个文件名段,作用与 docs/*.md 相同,不会跨过路径分隔符递归匹配子目录。要匹配 docs 下任意深度的 Markdown 文件,应写成 docs/**/*.md:让 ** 单独占据一个路径段,后面的 *.md 负责匹配文件名。

官方地址:https://github.com/bmatcuk/doublestar

下面围绕 v4 导入路径说明这一处语法差异。先区分模式的含义,再处理路径基准,避免把合法但含义不同的模式误判成库失效。

先检查双星号是否占据独立路径段

排查时按 / 把模式拆开。docs/**.md 只有 docs 和 **.md 两段;后者仍是一个文件名模式。星号数量增加,并不意味着这一段能够吞掉中间的目录分隔符。docs/**/*.md 则把目录层级和文件扩展名拆成了不同职责。

模式关注范围目录深度
docs/*.mddocs 的直接子项名称不跨目录
docs/**.md与上一行相同的文件名范围不跨目录
docs/**/*.mddocs 内符合名称条件的路径零层或更多目录

**/ 可以覆盖零层目录,因此递归写法也能匹配 docs/readme.md,不要求文件必须放进一层子目录。需要恰好一层子目录时,应表达为 docs/*/*.md,不能用递归模式代替深度限制。

双星号与文件名段的结构关系
双星号位置与路径段含义的静态说明图。

用直接子文件和深层文件分开判断

只拿一个 docs/readme.md 判断递归是否有效,证据不足,因为三种写法都可能覆盖它。应至少保留一个直接子文件、一个嵌套子文件和一个扩展名不同的文件。由模式规则可推导:两种文件名段写法可以匹配 readme.md,但无法匹配 guide/setup.md;递归写法覆盖这两个 Markdown 路径,不覆盖 setup.txt。

下面是完整示例,使用标准库的内存文件系统固定路径集合。代码用于读者自行复现,文中的判断来自官方模式规则,不是本次运行日志。

package main

import (
    "fmt"
    "log"
    "testing/fstest"

    "github.com/bmatcuk/doublestar/v4"
)

func main() {
    // 内存文件树用于说明模式含义,不依赖当前工作目录。
    files := fstest.MapFS{
        "docs/readme.md":        &fstest.MapFile{Data: []byte("root")},
        "docs/guide/setup.md":   &fstest.MapFile{Data: []byte("child")},
        "docs/guide/setup.txt":  &fstest.MapFile{Data: []byte("text")},
    }
    patterns := []string{"docs/**.md", "docs/**/*.md"}
    names := []string{"docs/readme.md", "docs/guide/setup.md"}

    for _, pattern := range patterns {
        for _, name := range names {
            // Match 只比较字符串,不读取文件是否存在。
            matched, err := doublestar.Match(pattern, name)
            if err != nil {
                log.Fatal(err)
            }
            fmt.Printf("pattern=%q name=%q matched=%t\n", pattern, name, matched)
        }
        // Glob 在给定文件系统内查找;模式相对于 files 的根目录。
        matches, err := doublestar.Glob(files, pattern)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Printf("pattern=%q files=%v\n", pattern, matches)
    }
}

依赖为 github.com/bmatcuk/doublestar/v4,可以放入已有 Go module,并通过项目的依赖管理方式引入。示例没有读取本机目录,也没有把输出内容当成实际扫描结果。若业务目录可能存在同名目录,注意 *.md 是名称条件,本身并不承诺结果一定是普通文件;需要排除目录时可在 Glob 中使用 WithFilesOnly(),并单独考虑符号链接规则。

再检查匹配的路径基准和 API

Match 比较模式和整个 name 字符串,不负责读取磁盘。即使某个路径不存在,只要字符串符合规则,也可以返回 true。不要在传入 Match 前只保留 Base 文件名,否则原来想判断的目录层级已经丢失。

Glob 则相对于传入的 fs.FS 根查找。假设使用 os.DirFS("docs"),模式就应是 **/*.md;若根是 docs 的父目录,才写 docs/**/*.md。把 docs 同时写进根目录和模式,实际查询的层级就变了。

Match 与 Glob 的路径基准结构
字符串匹配与文件集合查找的静态关系图。

Match 和 Glob 使用正斜杠作为分隔符。处理操作系统原生路径时,可以考虑 PathMatch 或 FilepathGlob;后者会整理路径并拆分基础路径。filepath.ToSlash 只替换当前平台识别的路径分隔符,不是任意外部路径格式的通用解析器。来自不同平台的配置数据应先约定格式。

错误为空并不表示模式符合递归意图

docs/**.md 是合法模式,所以不会仅因它没有递归含义就报错。err == nil 表示这次调用没有报告错误,并不表示查找范围符合业务预期。Glob 没有匹配项时可以返回空结果;默认还会忽略目录读取等文件系统错误,因此也不能单靠空集合断言目录里没有文件。

如果调用方需要感知读取错误,可按需求传入 WithFailOnIOErrors()。但一个不存在的固定路径默认仍被当成无匹配,相关固定前缀不存在的处理还应结合 WithFailOnPatternNotExist() 的具体语义。不要通过无条件扩大递归范围来掩盖根目录设置错误。

收尾检查清单与常见疑问

  • 确认是 **/*.md,而不是把 ** 与后缀连成一个文件名段。
  • 同时比较根层和嵌套层样本,避免只看直接子文件。
  • 确认传入的是完整相对路径,并且模式与 fs.FS 根目录相配。
  • 区分“字符串匹配成功”“查找到文件”和“读取错误未报告”。
  • 将递归范围限定在目标根目录,文件类型条件另外表达。

双星号越多,递归层数越多吗?不能这样理解。这里的关键是 ** 的独立路径段位置,不是星号数量对应目录层数。

递归写法会漏掉 docs 直属文件吗?不会因缺少中间目录而漏掉,因为该目录段允许零层。若仍为空,应回到路径基准和文件名条件排查。

能把 glob 当正则表达式吗?不能。先使用库支持的 glob 语法表达路径范围,额外筛选规则可另行处理,不要混入正则语义。

参考:Doublestar 官方模式规则、Glob 的路径与错误约定。

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