在日常开发中,文件与目录操作是高频需求。虽然 Go 标准库已经提供了 ospath/filepath 等基础能力,但直接使用仍需处理大量边界条件。本文将封装一个实用的 fileutil 工具包,涵盖常用操作:路径存在判断、目录创建、文件复制、目录复制、移动、删除、遍历、大小统计等。每个方法都有详细的注释和错误处理,可直接集成到项目中。

完整代码可在文末获取,或直接复制使用。

功能概览

核心代码:fileutil 包实现

// Package fileutil 提供文件和目录操作的常用工具函数
package fileutil

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

// Exists 判断路径是否存在(文件或目录均适用)
func Exists(path string) bool {
    _, err := os.Stat(path)
    return err == nil || !os.IsNotExist(err)
}

// IsDir 判断给定路径是否为目录
func IsDir(path string) (bool, error) {
    info, err := os.Stat(path)
    if err != nil {
        return false, err
    }
    return info.IsDir(), nil
}

// IsFile 判断给定路径是否为普通文件
func IsFile(path string) (bool, error) {
    info, err := os.Stat(path)
    if err != nil {
        return false, err
    }
    return !info.IsDir(), nil
}

// EnsureDir 确保目录存在,如果不存在则递归创建。
// 若已存在但不是目录,返回错误。
func EnsureDir(path string) error {
    if Exists(path) {
        isDir, err := IsDir(path)
        if err != nil {
            return err
        }
        if !isDir {
            return fmt.Errorf("path exists but is not a directory: %s", path)
        }
        return nil
    }
    return os.MkdirAll(path, 0755)
}

// CopyFile 复制单个文件从 src 到 dst。
// 如果 dst 已存在,默认会被覆盖。
// 复制时会保留源文件的权限模式。
func CopyFile(src, dst string) error {
    // 打开源文件
    srcFile, err := os.Open(src)
    if err != nil {
        return fmt.Errorf("open source file: %w", err)
    }
    defer srcFile.Close()

    // 获取源文件信息(用于保留权限)
    srcInfo, err := srcFile.Stat()
    if err != nil {
        return fmt.Errorf("stat source file: %w", err)
    }

    // 创建目标文件
    dstFile, err := os.Create(dst)
    if err != nil {
        return fmt.Errorf("create destination file: %w", err)
    }
    defer dstFile.Close()

    // 复制内容
    if _, err := io.Copy(dstFile, srcFile); err != nil {
        return fmt.Errorf("copy content: %w", err)
    }

    // 同步写入到磁盘
    if err := dstFile.Sync(); err != nil {
        return fmt.Errorf("sync destination file: %w", err)
    }

    // 保留源文件的权限模式
    return os.Chmod(dst, srcInfo.Mode())
}

// MoveFile 移动文件(也可用于重命名)。
// 如果跨文件系统移动失败,会自动降级为复制后删除。
func MoveFile(src, dst string) error {
    // 先尝试直接 rename(同一文件系统内最快)
    err := os.Rename(src, dst)
    if err == nil {
        return nil
    }

    // 跨文件系统时,采用复制+删除
    if err := CopyFile(src, dst); err != nil {
        return fmt.Errorf("copy file during move: %w", err)
    }
    if err := os.Remove(src); err != nil {
        // 复制成功但删除失败,目标文件已存在,记录错误但不回滚(避免数据丢失)
        return fmt.Errorf("remove source file after copy: %w", err)
    }
    return nil
}

// CopyDir 递归复制整个目录从 src 到 dst。
// 如果 dst 已存在且是目录,内容会合并;否则会先创建。
func CopyDir(src, dst string) error {
    // 获取源目录信息
    srcInfo, err := os.Stat(src)
    if err != nil {
        return fmt.Errorf("stat source directory: %w", err)
    }
    if !srcInfo.IsDir() {
        return fmt.Errorf("source is not a directory: %s", src)
    }

    // 创建目标目录(保留源目录权限)
    if err := os.MkdirAll(dst, srcInfo.Mode()); err != nil {
        return fmt.Errorf("create destination directory: %w", err)
    }

    // 遍历源目录
    entries, err := os.ReadDir(src)
    if err != nil {
        return fmt.Errorf("read source directory: %w", err)
    }

    for _, entry := range entries {
        srcPath := filepath.Join(src, entry.Name())
        dstPath := filepath.Join(dst, entry.Name())

        if entry.IsDir() {
            // 递归复制子目录
            if err := CopyDir(srcPath, dstPath); err != nil {
                return err
            }
        } else {
            // 复制文件
            if err := CopyFile(srcPath, dstPath); err != nil {
                return err
            }
        }
    }
    return nil
}

// DeleteDir 删除目录及其所有内容(类似 rm -rf)
func DeleteDir(path string) error {
    if !Exists(path) {
        return nil // 幂等:已不存在即成功
    }
    return os.RemoveAll(path)
}

// ListFiles 递归列出目录下所有文件的绝对路径。
// 如果指定了 extensions(例如 []string{".go", ".txt"}),则只返回匹配的文件。
// 传 nil 或空切片则返回所有文件。
func ListFiles(dir string, extensions []string) ([]string, error) {
    var files []string
    err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
        if err != nil {
            return err
        }
        if d.IsDir() {
            return nil
        }

        // 过滤扩展名
        if len(extensions) > 0 {
            ext := strings.ToLower(filepath.Ext(path))
            matched := false
            for _, e := range extensions {
                if strings.ToLower(e) == ext {
                    matched = true
                    break
                }
            }
            if !matched {
                return nil
            }
        }

        absPath, err := filepath.Abs(path)
        if err != nil {
            return err
        }
        files = append(files, absPath)
        return nil
    })
    return files, err
}

// DirSize 计算目录占用磁盘总大小(递归包含子目录)
func DirSize(path string) (int64, error) {
    var size int64
    err := filepath.WalkDir(path, func(_ string, d os.DirEntry, err error) error {
        if err != nil {
            return err
        }
        if !d.IsDir() {
            info, err := d.Info()
            if err != nil {
                return err
            }
            size += info.Size()
        }
        return nil
    })
    return size, err
}

// ReadString 快速读取整个文件为字符串
func ReadString(path string) (string, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return "", err
    }
    return string(data), nil
}

// WriteString 快速将字符串写入文件(覆盖写入,权限 0644)
func WriteString(path, content string) error {
    return os.WriteFile(path, []byte(content), 0644)
}

// AppendString 追加字符串到文件末尾,如果文件不存在则创建
func AppendString(path, content string) error {
    f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        return err
    }
    defer f.Close()
    _, err = f.WriteString(content)
    return err
}

// SafeWrite 原子性写入:先写临时文件,再 rename 替换,避免写入中途崩溃导致文件损坏。
func SafeWrite(path string, data []byte, perm os.FileMode) error {
    // 在同目录下创建临时文件
    tmpFile, err := os.CreateTemp(filepath.Dir(path), ".tmp-*")
    if err != nil {
        return err
    }
    tmpName := tmpFile.Name()

    // 写入数据
    if _, err := tmpFile.Write(data); err != nil {
        tmpFile.Close()
        os.Remove(tmpName)
        return err
    }
    if err := tmpFile.Sync(); err != nil {
        tmpFile.Close()
        os.Remove(tmpName)
        return err
    }
    if err := tmpFile.Close(); err != nil {
        os.Remove(tmpName)
        return err
    }

    // 设置权限
    if err := os.Chmod(tmpName, perm); err != nil {
        os.Remove(tmpName)
        return err
    }

    // 原子替换
    return os.Rename(tmpName, path)
}

使用示例

package main

import (
    "fmt"
    "log"
    "path/to/fileutil" // 替换为你的实际导入路径
)

func main() {
    // 1. 判断路径是否存在
    fmt.Println("存在:", fileutil.Exists("/tmp/test"))

    // 2. 确保目录存在
    if err := fileutil.EnsureDir("./data/logs"); err != nil {
        log.Fatal(err)
    }

    // 3. 写入文件
    if err := fileutil.WriteString("./data/config.txt", "Hello, Gopher!"); err != nil {
        log.Fatal(err)
    }

    // 4. 安全写入(原子操作)
    if err := fileutil.SafeWrite("./data/important.dat", []byte("atomic write"), 0644); err != nil {
        log.Fatal(err)
    }

    // 5. 复制文件
    if err := fileutil.CopyFile("./data/config.txt", "./data/backup/config_copy.txt"); err != nil {
        log.Fatal(err)
    }

    // 6. 复制目录
    if err := fileutil.CopyDir("./data", "./data_backup"); err != nil {
        log.Fatal(err)
    }

    // 7. 列出所有 .go 文件
    goFiles, err := fileutil.ListFiles(".", []string{".go"})
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("找到 %d 个 Go 文件\n", len(goFiles))

    // 8. 计算目录大小
    size, err := fileutil.DirSize("./data")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("目录大小: %.2f MB\n", float64(size)/1024/1024)

    // 9. 移动文件
    if err := fileutil.MoveFile("./data/old.txt", "./data/archive/new.txt"); err != nil {
        log.Fatal(err)
    }

    // 10. 删除目录
    if err := fileutil.DeleteDir("./data_backup"); err != nil {
        log.Fatal(err)
    }
}

要点说明

函数注意事项
CopyFile覆盖已存在的目标文件;保留源文件权限
MoveFile跨文件系统自动降级为复制+删除
CopyDir递归复制,目标目录存在时会合并内容
SafeWrite原子写入,先写临时文件再 rename,防止写一半时进程崩溃导致数据损坏
ListFiles支持按扩展名过滤,返回绝对路径
EnsureDir已存在但不是目录时报错,避免误用

这个工具包覆盖了日常开发中 90% 的文件目录操作场景。你可以根据项目需要继续扩展,比如增加压缩/解压、权限批量修改等功能。直接拷贝到项目中使用即可。