在日常开发中,文件与目录操作是高频需求。虽然 Go 标准库已经提供了 os、path/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% 的文件目录操作场景。你可以根据项目需要继续扩展,比如增加压缩/解压、权限批量修改等功能。直接拷贝到项目中使用即可。
