4.1.2 文件系统遍历

4.1.2 文件系统遍历 #

文件系统遍历是系统编程中的重要技能,它允许我们递归地访问目录结构中的所有文件和子目录。Go 语言提供了多种方式来实现文件系统遍历,从简单的目录读取到高效的递归遍历。本节将详细介绍各种文件系统遍历技术和最佳实践。

目录操作基础 #

读取目录内容 #

Go 语言提供了多种方法来读取目录内容:

package main

import (
    "fmt"
    "os"
)

func readDirectoryBasic() {
    // 方法1: 使用 os.ReadDir (Go 1.16+)
    entries, err := os.ReadDir(".")
    if err != nil {
        fmt.Printf("读取目录失败: %v\n", err)
        return
    }

    fmt.Println("当前目录内容:")
    for _, entry := range entries {
        if entry.IsDir() {
            fmt.Printf("[DIR]  %s\n", entry.Name())
        } else {
            fmt.Printf("[FILE] %s\n", entry.Name())
        }
    }
}

func readDirectoryWithInfo() {
    // 方法2: 使用 os.File.Readdir
    dir, err := os.Open(".")
    if err != nil {
        fmt.Printf("打开目录失败: %v\n", err)
        return
    }
    defer dir.Close()

    fileInfos, err := dir.Readdir(-1) // -1 表示读取所有条目
    if err != nil {
        fmt.Printf("读取目录信息失败: %v\n", err)
        return
    }

    fmt.Println("目录详细信息:")
    for _, info := range fileInfos {
        fmt.Printf("名称: %s, 大小: %d, 修改时间: %v, 是否目录: %v\n",
            info.Name(), info.Size(), info.ModTime(), info.IsDir())
    }
}

创建和删除目录 #

func directoryOperations() {
    // 创建单个目录
    err := os.Mkdir("test_dir", 0755)
    if err != nil {
        fmt.Printf("创建目录失败: %v\n", err)
    } else {
        fmt.Println("目录创建成功")
    }

    // 创建多级目录
    err = os.MkdirAll("path/to/nested/dir", 0755)
    if err != nil {
        fmt.Printf("创建多级目录失败: %v\n", err)
    } else {
        fmt.Println("多级目录创建成功")
    }

    // 删除空目录
    err = os.Remove("test_dir")
    if err != nil {
        fmt.Printf("删除目录失败: %v\n", err)
    } else {
        fmt.Println("目录删除成功")
    }

    // 递归删除目录及其内容
    err = os.RemoveAll("path")
    if err != nil {
        fmt.Printf("递归删除失败: %v\n", err)
    } else {
        fmt.Println("递归删除成功")
    }
}

使用 filepath.Walk 进行遍历 #

基础遍历 #

filepath.Walk 是 Go 语言中最常用的文件系统遍历函数:

package main

import (
    "fmt"
    "os"
    "path/filepath"
)

func basicWalk() {
    root := "."

    err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
        if err != nil {
            fmt.Printf("访问 %s 时出错: %v\n", path, err)
            return err
        }

        // 打印文件信息
        if info.IsDir() {
            fmt.Printf("[DIR]  %s\n", path)
        } else {
            fmt.Printf("[FILE] %s (大小: %d 字节)\n", path, info.Size())
        }

        return nil
    })

    if err != nil {
        fmt.Printf("遍历失败: %v\n", err)
    }
}

高级遍历示例 #

func advancedWalk() {
    root := "/path/to/search"
    var totalSize int64
    fileCount := 0
    dirCount := 0

    err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
        if err != nil {
            // 记录错误但继续遍历
            fmt.Printf("警告: 无法访问 %s: %v\n", path, err)
            return nil
        }

        if info.IsDir() {
            dirCount++
            fmt.Printf("进入目录: %s\n", path)
        } else {
            fileCount++
            totalSize += info.Size()

            // 根据文件扩展名进行分类
            ext := filepath.Ext(path)
            switch ext {
            case ".go":
                fmt.Printf("Go 源文件: %s\n", path)
            case ".txt":
                fmt.Printf("文本文件: %s\n", path)
            case ".jpg", ".png", ".gif":
                fmt.Printf("图片文件: %s\n", path)
            default:
                fmt.Printf("其他文件: %s\n", path)
            }
        }

        return nil
    })

    if err != nil {
        fmt.Printf("遍历失败: %v\n", err)
        return
    }

    fmt.Printf("\n遍历统计:\n")
    fmt.Printf("目录数量: %d\n", dirCount)
    fmt.Printf("文件数量: %d\n", fileCount)
    fmt.Printf("总大小: %d 字节 (%.2f MB)\n", totalSize, float64(totalSize)/(1024*1024))
}

条件过滤遍历 #

func filteredWalk() {
    root := "."

    err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }

        // 跳过隐藏文件和目录
        if filepath.Base(path)[0] == '.' && path != root {
            if info.IsDir() {
                return filepath.SkipDir // 跳过整个目录
            }
            return nil // 跳过文件
        }

        // 只处理 Go 源文件
        if !info.IsDir() && filepath.Ext(path) == ".go" {
            fmt.Printf("Go 文件: %s\n", path)

            // 可以在这里进行文件内容分析
            analyzeGoFile(path)
        }

        return nil
    })

    if err != nil {
        fmt.Printf("遍历失败: %v\n", err)
    }
}

func analyzeGoFile(path string) {
    file, err := os.Open(path)
    if err != nil {
        fmt.Printf("无法打开文件 %s: %v\n", path, err)
        return
    }
    defer file.Close()

    // 简单的行数统计
    scanner := bufio.NewScanner(file)
    lineCount := 0
    for scanner.Scan() {
        lineCount++
    }

    fmt.Printf("  -> %d 行代码\n", lineCount)
}

使用 filepath.WalkDir 进行高效遍历 #

Go 1.16 引入了 filepath.WalkDir,它比 filepath.Walk 更高效:

func efficientWalk() {
    root := "."

    err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
        if err != nil {
            fmt.Printf("访问 %s 时出错: %v\n", path, err)
            return err
        }

        if d.IsDir() {
            fmt.Printf("[DIR]  %s\n", path)
        } else {
            // 只在需要时获取详细信息
            info, err := d.Info()
            if err != nil {
                fmt.Printf("获取文件信息失败: %v\n", err)
                return nil
            }
            fmt.Printf("[FILE] %s (大小: %d 字节)\n", path, info.Size())
        }

        return nil
    })

    if err != nil {
        fmt.Printf("遍历失败: %v\n", err)
    }
}

自定义遍历实现 #

递归遍历实现 #

func customWalk(root string, visitFunc func(path string, info os.FileInfo) error) error {
    return customWalkRecursive(root, visitFunc)
}

func customWalkRecursive(path string, visitFunc func(string, os.FileInfo) error) error {
    info, err := os.Stat(path)
    if err != nil {
        return err
    }

    // 访问当前路径
    if err := visitFunc(path, info); err != nil {
        return err
    }

    // 如果是目录,递归遍历子项
    if info.IsDir() {
        entries, err := os.ReadDir(path)
        if err != nil {
            return err
        }

        for _, entry := range entries {
            childPath := filepath.Join(path, entry.Name())
            if err := customWalkRecursive(childPath, visitFunc); err != nil {
                return err
            }
        }
    }

    return nil
}

func testCustomWalk() {
    err := customWalk(".", func(path string, info os.FileInfo) error {
        if info.IsDir() {
            fmt.Printf("[DIR]  %s\n", path)
        } else {
            fmt.Printf("[FILE] %s\n", path)
        }
        return nil
    })

    if err != nil {
        fmt.Printf("自定义遍历失败: %v\n", err)
    }
}

并发遍历实现 #

package main

import (
    "fmt"
    "os"
    "path/filepath"
    "sync"
)

type WalkResult struct {
    Path string
    Info os.FileInfo
    Err  error
}

func concurrentWalk(root string, workers int) <-chan WalkResult {
    results := make(chan WalkResult)
    paths := make(chan string)

    // 启动工作协程
    var wg sync.WaitGroup
    for i := 0; i < workers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for path := range paths {
                info, err := os.Stat(path)
                results <- WalkResult{
                    Path: path,
                    Info: info,
                    Err:  err,
                }
            }
        }()
    }

    // 启动路径发现协程
    go func() {
        defer close(paths)
        filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
            if err != nil {
                results <- WalkResult{Path: path, Err: err}
                return nil
            }
            paths <- path
            return nil
        })
    }()

    // 启动结果关闭协程
    go func() {
        wg.Wait()
        close(results)
    }()

    return results
}

func testConcurrentWalk() {
    results := concurrentWalk(".", 4)

    for result := range results {
        if result.Err != nil {
            fmt.Printf("错误: %s - %v\n", result.Path, result.Err)
            continue
        }

        if result.Info.IsDir() {
            fmt.Printf("[DIR]  %s\n", result.Path)
        } else {
            fmt.Printf("[FILE] %s\n", result.Path)
        }
    }
}

实际应用示例 #

文件搜索工具 #

package main

import (
    "fmt"
    "os"
    "path/filepath"
    "regexp"
    "strings"
    "time"
)

type SearchCriteria struct {
    NamePattern   *regexp.Regexp
    SizeMin       int64
    SizeMax       int64
    ModifiedAfter time.Time
    ModifiedBefore time.Time
    FileType      string // "file", "dir", "all"
}

type SearchResult struct {
    Path     string
    Info     os.FileInfo
    Matches  []string
}

func searchFiles(root string, criteria SearchCriteria) ([]SearchResult, error) {
    var results []SearchResult

    err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return nil // 跳过错误,继续遍历
        }

        // 检查文件类型
        if criteria.FileType == "file" && info.IsDir() {
            return nil
        }
        if criteria.FileType == "dir" && !info.IsDir() {
            return nil
        }

        // 检查名称模式
        if criteria.NamePattern != nil {
            if !criteria.NamePattern.MatchString(filepath.Base(path)) {
                return nil
            }
        }

        // 检查文件大小
        if !info.IsDir() {
            if criteria.SizeMin > 0 && info.Size() < criteria.SizeMin {
                return nil
            }
            if criteria.SizeMax > 0 && info.Size() > criteria.SizeMax {
                return nil
            }
        }

        // 检查修改时间
        if !criteria.ModifiedAfter.IsZero() && info.ModTime().Before(criteria.ModifiedAfter) {
            return nil
        }
        if !criteria.ModifiedBefore.IsZero() && info.ModTime().After(criteria.ModifiedBefore) {
            return nil
        }

        // 符合条件,添加到结果
        results = append(results, SearchResult{
            Path: path,
            Info: info,
        })

        return nil
    })

    return results, err
}

func main() {
    // 搜索所有 .go 文件
    namePattern, _ := regexp.Compile(`\.go$`)
    criteria := SearchCriteria{
        NamePattern: namePattern,
        FileType:    "file",
        SizeMin:     100, // 至少 100 字节
    }

    results, err := searchFiles(".", criteria)
    if err != nil {
        fmt.Printf("搜索失败: %v\n", err)
        return
    }

    fmt.Printf("找到 %d 个匹配的文件:\n", len(results))
    for _, result := range results {
        fmt.Printf("%s (大小: %d 字节, 修改时间: %v)\n",
            result.Path, result.Info.Size(), result.Info.ModTime())
    }
}

目录同步工具 #

type SyncOperation struct {
    Type string // "copy", "delete", "update"
    Src  string
    Dst  string
    Info os.FileInfo
}

func syncDirectories(srcDir, dstDir string) error {
    operations, err := planSyncOperations(srcDir, dstDir)
    if err != nil {
        return err
    }

    fmt.Printf("计划执行 %d 个同步操作\n", len(operations))

    for _, op := range operations {
        if err := executeSyncOperation(op); err != nil {
            fmt.Printf("执行操作失败: %v\n", err)
            continue
        }
        fmt.Printf("完成: %s %s\n", op.Type, op.Src)
    }

    return nil
}

func planSyncOperations(srcDir, dstDir string) ([]SyncOperation, error) {
    var operations []SyncOperation
    srcFiles := make(map[string]os.FileInfo)
    dstFiles := make(map[string]os.FileInfo)

    // 扫描源目录
    err := filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }
        relPath, _ := filepath.Rel(srcDir, path)
        srcFiles[relPath] = info
        return nil
    })
    if err != nil {
        return nil, err
    }

    // 扫描目标目录
    if _, err := os.Stat(dstDir); err == nil {
        err = filepath.Walk(dstDir, func(path string, info os.FileInfo, err error) error {
            if err != nil {
                return err
            }
            relPath, _ := filepath.Rel(dstDir, path)
            dstFiles[relPath] = info
            return nil
        })
        if err != nil {
            return nil, err
        }
    }

    // 计划操作
    for relPath, srcInfo := range srcFiles {
        srcPath := filepath.Join(srcDir, relPath)
        dstPath := filepath.Join(dstDir, relPath)

        if dstInfo, exists := dstFiles[relPath]; exists {
            // 文件存在,检查是否需要更新
            if !srcInfo.IsDir() && (srcInfo.Size() != dstInfo.Size() ||
                srcInfo.ModTime().After(dstInfo.ModTime())) {
                operations = append(operations, SyncOperation{
                    Type: "update",
                    Src:  srcPath,
                    Dst:  dstPath,
                    Info: srcInfo,
                })
            }
        } else {
            // 文件不存在,需要复制
            operations = append(operations, SyncOperation{
                Type: "copy",
                Src:  srcPath,
                Dst:  dstPath,
                Info: srcInfo,
            })
        }
    }

    // 检查需要删除的文件
    for relPath := range dstFiles {
        if _, exists := srcFiles[relPath]; !exists {
            operations = append(operations, SyncOperation{
                Type: "delete",
                Dst:  filepath.Join(dstDir, relPath),
            })
        }
    }

    return operations, nil
}

func executeSyncOperation(op SyncOperation) error {
    switch op.Type {
    case "copy", "update":
        if op.Info.IsDir() {
            return os.MkdirAll(op.Dst, op.Info.Mode())
        } else {
            return copyFile(op.Src, op.Dst)
        }
    case "delete":
        return os.RemoveAll(op.Dst)
    }
    return nil
}

func copyFile(src, dst string) error {
    // 确保目标目录存在
    if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
        return err
    }

    srcFile, err := os.Open(src)
    if err != nil {
        return err
    }
    defer srcFile.Close()

    dstFile, err := os.Create(dst)
    if err != nil {
        return err
    }
    defer dstFile.Close()

    _, err = io.Copy(dstFile, srcFile)
    return err
}

性能优化技巧 #

避免重复的 Stat 调用 #

func optimizedWalk() {
    err := filepath.WalkDir(".", func(path string, d os.DirEntry, err error) error {
        if err != nil {
            return err
        }

        // 只在需要详细信息时调用 Info()
        if d.IsDir() {
            fmt.Printf("[DIR] %s\n", path)
        } else {
            // 对于文件,只在需要时获取详细信息
            if strings.HasSuffix(path, ".log") {
                info, err := d.Info()
                if err != nil {
                    return err
                }
                fmt.Printf("[LOG] %s (大小: %d)\n", path, info.Size())
            } else {
                fmt.Printf("[FILE] %s\n", path)
            }
        }

        return nil
    })

    if err != nil {
        fmt.Printf("遍历失败: %v\n", err)
    }
}

使用缓存减少系统调用 #

type CachedWalker struct {
    cache map[string]os.FileInfo
    mutex sync.RWMutex
}

func NewCachedWalker() *CachedWalker {
    return &CachedWalker{
        cache: make(map[string]os.FileInfo),
    }
}

func (cw *CachedWalker) GetInfo(path string) (os.FileInfo, error) {
    cw.mutex.RLock()
    if info, exists := cw.cache[path]; exists {
        cw.mutex.RUnlock()
        return info, nil
    }
    cw.mutex.RUnlock()

    info, err := os.Stat(path)
    if err != nil {
        return nil, err
    }

    cw.mutex.Lock()
    cw.cache[path] = info
    cw.mutex.Unlock()

    return info, nil
}

小结 #

本节详细介绍了 Go 语言中的文件系统遍历技术,包括:

  1. 目录操作基础 - 读取、创建和删除目录的基本方法
  2. filepath.Walk - 标准库提供的递归遍历函数
  3. filepath.WalkDir - 更高效的遍历方法
  4. 自定义遍历 - 实现定制化的遍历逻辑
  5. 并发遍历 - 提高大型目录结构的遍历性能
  6. 实际应用 - 文件搜索和目录同步等实用工具
  7. 性能优化 - 减少系统调用和提高遍历效率的技巧

掌握这些技术后,你就能够高效地处理各种文件系统遍历需求。在下一节中,我们将学习文件权限与属性的管理。