Go 文件处理入门:读写文件、目录遍历和路径操作

发布时间:2026/7/3 17:04:04

Go 文件处理入门:读写文件、目录遍历和路径操作 Go 标准库已经提供了完整工具。新手最常用的是这几个包os io bufio path/filepath errors这篇文章会从最常见的场景开始一步一步讲清楚 Go 文件处理。先记住几个核心包osos包负责和操作系统交互。常用能力包括读文件os.ReadFile写文件os.WriteFile打开文件os.Open、os.OpenFile创建文件os.Create创建目录os.Mkdir、os.MkdirAll删除文件os.Remove删除目录树os.RemoveAll查看文件信息os.Stat读取目录os.ReadDir创建临时文件/目录os.CreateTemp、os.MkdirTempioio包处理通用输入输出。最重要的概念是io.Reader io.Writer很多东西都可以是Reader或Writer文件网络连接字符串读取器bytes.BufferHTTP 响应体比如复制文件时常用io.Copy(dst, src)bufiobufio包提供带缓冲的读写。常见用途按行读取文件bufio.Scanner更高效地写入多行内容bufio.Writerpath/filepathpath/filepath包负责文件路径处理。常用能力拼接路径filepath.Join获取文件名filepath.Base获取目录filepath.Dir获取扩展名filepath.Ext清理路径filepath.Clean遍历目录树filepath.WalkDir注意处理本地文件路径时优先用 path/filepath。 处理 URL 路径时才更多使用 path。文件处理的基本原则新手可以先记住四条小文件可以一次性读写。大文件不要一次性全部读进内存。打开的文件要关闭。错误要处理不要随手丢掉。比如file, err : os.Open(data.txt) if err ! nil { return err } defer file.Close()defer file.Close()的意思是函数返回前关闭文件。这样无论后面成功还是出错文件资源都会被释放。一、读取整个文件os.ReadFile如果文件不大最简单的读取方式是os.ReadFile。它会一次性把整个文件读到内存里返回[]byte。完整例子package main import ( fmt os path/filepath ) func main() { dir, err : os.MkdirTemp(, go-file-demo-*) if err ! nil { fmt.Println(create temp dir:, err) return } defer os.RemoveAll(dir) path : filepath.Join(dir, hello.txt) err os.WriteFile(path, []byte(hello go\n), 0644) if err ! nil { fmt.Println(write file:, err) return } data, err : os.ReadFile(path) if err ! nil { fmt.Println(read file:, err) return } fmt.Print(string(data)) }输出hello go这里有几个点data, err : os.ReadFile(path)data的类型是[]byte。如果你确定文件内容是文本可以转成字符串text : string(data)如果文件是图片、压缩包、音频等二进制文件就不要随便转字符串直接处理[]byte。二、写入整个文件os.WriteFile写小文件可以用os.WriteFile。package main import ( fmt os path/filepath ) func main() { dir, err : os.MkdirTemp(, go-file-demo-*) if err ! nil { fmt.Println(create temp dir:, err) return } defer os.RemoveAll(dir) path : filepath.Join(dir, note.txt) content : []byte(first line\nsecond line\n) err os.WriteFile(path, content, 0644) if err ! nil { fmt.Println(write file:, err) return } data, err : os.ReadFile(path) if err ! nil { fmt.Println(read file:, err) return } fmt.Print(string(data)) }输出first line second line第三个参数0644是文件权限。可以先这样理解0644文件拥有者可读写其他人只读 0600只有文件拥有者可读写 0755常用于目录或可执行文件拥有者可读写执行其他人可读执行注意权限参数只在创建文件时有意义。 如果文件已经存在os.WriteFile 会截断原文件内容再写入新内容。也就是说下面这行不是追加而是覆盖os.WriteFile(path, []byte(new content), 0644)三、追加内容os.OpenFile如果你想在文件末尾追加内容不要用os.WriteFile。要用os.OpenFile搭配标志位。package main import ( fmt os path/filepath ) func appendLine(path string, line string) error { file, err : os.OpenFile( path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644, ) if err ! nil { return fmt.Errorf(open file for append: %w, err) } defer file.Close() if _, err : file.WriteString(line \n); err ! nil { return fmt.Errorf(append line: %w, err) } return nil } func main() { dir, err : os.MkdirTemp(, go-file-demo-*) if err ! nil { fmt.Println(create temp dir:, err) return } defer os.RemoveAll(dir) path : filepath.Join(dir, app.log) if err : appendLine(path, start); err ! nil { fmt.Println(err) return } if err : appendLine(path, finish); err ! nil { fmt.Println(err) return } data, err : os.ReadFile(path) if err ! nil { fmt.Println(read file:, err) return } fmt.Print(string(data)) }输出start finish这里的标志位含义是os.O_CREATE // 文件不存在就创建 os.O_WRONLY // 只写 os.O_APPEND // 写入时追加到文件末尾多个标志位用|组合os.O_CREATE | os.O_WRONLY | os.O_APPEND常见标志位还有os.O_RDONLY // 只读 os.O_RDWR // 读写 os.O_TRUNC // 打开时清空文件 os.O_EXCL // 和 O_CREATE 一起用要求文件必须不存在五、分块读取大文件如果文件很大不适合用os.ReadFile一次性读入内存。这时可以用file.Read分块读取。package main import ( fmt io os path/filepath ) func readByChunks(path string) error { file, err : os.Open(path) if err ! nil { return fmt.Errorf(open file: %w, err) } defer file.Close() buffer : make([]byte, 5) for { n, err : file.Read(buffer) if n 0 { fmt.Printf(chunk: %q\n, buffer[:n]) } if err io.EOF { break } if err ! nil { return fmt.Errorf(read file: %w, err) } } return nil } func main() { dir, err : os.MkdirTemp(, go-file-demo-*) if err ! nil { fmt.Println(create temp dir:, err) return } defer os.RemoveAll(dir) path : filepath.Join(dir, data.txt) err os.WriteFile(path, []byte(hello gopher), 0644) if err ! nil { fmt.Println(write file:, err) return } if err : readByChunks(path); err ! nil { fmt.Println(err) } }输出chunk: hello chunk: goph chunk: er这里要注意io.EOF。io.EOF表示已经读到文件末尾。它不是坏事而是告诉你没有更多数据了。所以常见模式是if err io.EOF { break } if err ! nil { return err }六、逐行读取文件bufio.Scanner处理日志、CSV、配置文件时经常要按行读取。这时可以用bufio.Scanner。package main import ( bufio fmt os path/filepath ) func printLines(path string) error { file, err : os.Open(path) if err ! nil { return fmt.Errorf(open file: %w, err) } defer file.Close() scanner : bufio.NewScanner(file) lineNumber : 1 for scanner.Scan() { fmt.Printf(%d: %s\n, lineNumber, scanner.Text()) lineNumber } if err : scanner.Err(); err ! nil { return fmt.Errorf(scan file: %w, err) } return nil } func main() { dir, err : os.MkdirTemp(, go-file-demo-*) if err ! nil { fmt.Println(create temp dir:, err) return } defer os.RemoveAll(dir) path : filepath.Join(dir, names.txt) err os.WriteFile(path, []byte(Alice\nBob\nCharlie\n), 0644) if err ! nil { fmt.Println(write file:, err) return } if err : printLines(path); err ! nil { fmt.Println(err) } }输出1: Alice 2: Bob 3: Charliescanner.Scan()每次读取一行。scanner.Text()获取当前行的文本。循环结束后要检查scanner.Err()因为循环结束可能有两种原因正常读完 读取过程中出错scanner.Err()可以帮你区分。Scanner 的限制bufio.Scanner很适合读取普通文本行。但它有默认 token 大小限制。如果一行特别长比如超长 JSON、超长日志、超长 base64可能需要调整缓冲区或者改用bufio.Reader。新手阶段先记住普通按行读取bufio.Scanner 很方便。 超长行或复杂分隔考虑 bufio.Reader。七、带缓冲写入bufio.Writer如果你要写很多小片段直接多次写文件可能不够高效。可以使用bufio.Writer先写到缓冲区最后Flush。package main import ( bufio fmt os path/filepath ) func writeLines(path string, lines []string) error { file, err : os.Create(path) if err ! nil { return fmt.Errorf(create file: %w, err) } defer file.Close() writer : bufio.NewWriter(file) for _, line : range lines { if _, err : writer.WriteString(line \n); err ! nil { return fmt.Errorf(write line: %w, err) } } if err : writer.Flush(); err ! nil { return fmt.Errorf(flush writer: %w, err) } return nil } func main() { dir, err : os.MkdirTemp(, go-file-demo-*) if err ! nil { fmt.Println(create temp dir:, err) return } defer os.RemoveAll(dir) path : filepath.Join(dir, tasks.txt) err writeLines(path, []string{learn go, write code, read docs}) if err ! nil { fmt.Println(err) return } data, err : os.ReadFile(path) if err ! nil { fmt.Println(read file:, err) return } fmt.Print(string(data)) }输出learn go write code read docs这里一定要调用writer.Flush()否则最后一部分数据可能还留在缓冲区里没有真正写入文件。八、复制文件io.Copy复制文件时不需要自己手写循环。可以使用io.Copy。package main import ( fmt io os path/filepath ) func copyFile(srcPath, dstPath string) error { src, err : os.Open(srcPath) if err ! nil { return fmt.Errorf(open source file: %w, err) } defer src.Close() dst, err : os.Create(dstPath) if err ! nil { return fmt.Errorf(create destination file: %w, err) } defer dst.Close() if _, err : io.Copy(dst, src); err ! nil { return fmt.Errorf(copy file: %w, err) } return nil } func main() { dir, err : os.MkdirTemp(, go-file-demo-*) if err ! nil { fmt.Println(create temp dir:, err) return } defer os.RemoveAll(dir) src : filepath.Join(dir, source.txt) dst : filepath.Join(dir, backup.txt) err os.WriteFile(src, []byte(important data\n), 0644) if err ! nil { fmt.Println(write source:, err) return } if err : copyFile(src, dst); err ! nil { fmt.Println(err) return } data, err : os.ReadFile(dst) if err ! nil { fmt.Println(read backup:, err) return } fmt.Print(string(data)) }输出important dataio.Copy(dst, src)的意思是从 src 读数据写入 dst直到读完或出错。它依赖的是接口io.Reader io.Writer所以它不仅能复制文件也能把网络响应写入文件把字符串写入 buffer等等。九、判断文件是否存在可以用os.Stat获取文件信息。如果文件不存在错误可以用errors.Is(err, os.ErrNotExist)判断。package main import ( errors fmt os path/filepath ) func exists(path string) (bool, error) { _, err : os.Stat(path) if err nil { return true, nil } if errors.Is(err, os.ErrNotExist) { return false, nil } return false, err } func main() { dir, err : os.MkdirTemp(, go-file-demo-*) if err ! nil { fmt.Println(create temp dir:, err) return } defer os.RemoveAll(dir) path : filepath.Join(dir, missing.txt) ok, err : exists(path) if err ! nil { fmt.Println(check file:, err) return } fmt.Println(exists:, ok) }输出exists: false不要用字符串判断strings.Contains(err.Error(), no such file)更推荐errors.Is(err, os.ErrNotExist)这和前面错误处理文章里的思路一致用结构化方式判断错误。十、获取文件信息os.Statos.Stat可以拿到文件大小、权限、是否目录等信息。package main import ( fmt os path/filepath ) func main() { dir, err : os.MkdirTemp(, go-file-demo-*) if err ! nil { fmt.Println(create temp dir:, err) return } defer os.RemoveAll(dir) path : filepath.Join(dir, info.txt) err os.WriteFile(path, []byte(hello), 0644) if err ! nil { fmt.Println(write file:, err) return } info, err : os.Stat(path) if err ! nil { fmt.Println(stat file:, err) return } fmt.Println(name:, info.Name()) fmt.Println(size:, info.Size()) fmt.Println(is dir:, info.IsDir()) fmt.Println(mode:, info.Mode()) }可能输出name: info.txt size: 5 is dir: false mode: -rw-r--r--常用方法info.Name() // 文件名 info.Size() // 文件大小单位是字节 info.Mode() // 文件权限和类型 info.IsDir() // 是否目录十一、创建目录os.Mkdir 和 os.MkdirAll创建单层目录可以用os.Mkdir。创建多层目录更常用os.MkdirAll。package main import ( fmt os path/filepath ) func main() { dir, err : os.MkdirTemp(, go-file-demo-*) if err ! nil { fmt.Println(create temp dir:, err) return } defer os.RemoveAll(dir) nested : filepath.Join(dir, logs, 2026, 06) if err : os.MkdirAll(nested, 0755); err ! nil { fmt.Println(mkdir all:, err) return } fmt.Println(created:, filepath.Base(nested)) }输出created: 06os.MkdirAll的特点是中间目录不存在就一起创建。 目录已经存在也不会报错。这对创建配置目录、缓存目录、日志目录很方便。十二、读取目录os.ReadDir读取一个目录下的文件和子目录可以用os.ReadDir。package main import ( fmt os path/filepath ) func main() { dir, err : os.MkdirTemp(, go-file-demo-*) if err ! nil { fmt.Println(create temp dir:, err) return } defer os.RemoveAll(dir) _ os.WriteFile(filepath.Join(dir, a.txt), []byte(a), 0644) _ os.WriteFile(filepath.Join(dir, b.txt), []byte(b), 0644) _ os.Mkdir(filepath.Join(dir, logs), 0755) entries, err : os.ReadDir(dir) if err ! nil { fmt.Println(read dir:, err) return } for _, entry : range entries { kind : file if entry.IsDir() { kind dir } fmt.Printf(%s: %s\n, kind, entry.Name()) } }可能输出file: a.txt file: b.txt dir: logsos.ReadDir只读取当前目录这一层。如果你要递归遍历子目录要用filepath.WalkDir。十三、递归遍历目录filepath.WalkDirfilepath.WalkDir可以递归遍历目录树。package main import ( fmt io/fs os path/filepath ) func main() { root, err : os.MkdirTemp(, go-file-demo-*) if err ! nil { fmt.Println(create temp dir:, err) return } defer os.RemoveAll(root) _ os.MkdirAll(filepath.Join(root, logs, app), 0755) _ os.WriteFile(filepath.Join(root, README.md), []byte(readme), 0644) _ os.WriteFile(filepath.Join(root, logs, app, today.log), []byte(ok), 0644) err filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { if err ! nil { return err } rel, err : filepath.Rel(root, path) if err ! nil { return err } if rel . { return nil } fmt.Println(rel) return nil }) if err ! nil { fmt.Println(walk dir:, err) } }可能输出README.md logs logs/app logs/app/today.log回调函数里有三个参数func(path string, d fs.DirEntry, err error) error含义是path当前路径d当前目录项信息err访问当前路径时遇到的错误如果回调返回错误遍历会停止并把错误返回给外层。十四、路径拼接filepath.Join不要手动拼接路径。不推荐path : dir / name更推荐path : filepath.Join(dir, name)完整例子package main import ( fmt path/filepath ) func main() { path : filepath.Join(data, logs, app.log) fmt.Println(path) fmt.Println(dir:, filepath.Dir(path)) fmt.Println(base:, filepath.Base(path)) fmt.Println(ext:, filepath.Ext(path)) }在类 Unix 系统上输出data/logs/app.log dir: data/logs base: app.log ext: .logfilepath.Join会使用当前操作系统合适的路径分隔符。所以跨平台处理本地文件路径时优先用它。十五、清理和转换路径filepath.Clean可以清理路径里的多余部分。package main import ( fmt path/filepath ) func main() { raw : data/./logs/../app.log fmt.Println(filepath.Clean(raw)) abs, err : filepath.Abs(raw) if err ! nil { fmt.Println(abs path:, err) return } fmt.Println(filepath.Base(abs)) }可能输出data/app.log app.log常用函数filepath.Clean(path) // 清理路径 filepath.Abs(path) // 转成绝对路径 filepath.Rel(base, target) // 计算相对路径十六、重命名和删除文件重命名文件使用os.Rename。删除文件使用os.Remove。删除目录树使用os.RemoveAll。package main import ( errors fmt os path/filepath ) func main() { dir, err : os.MkdirTemp(, go-file-demo-*) if err ! nil { fmt.Println(create temp dir:, err) return } defer os.RemoveAll(dir) oldPath : filepath.Join(dir, old.txt) newPath : filepath.Join(dir, new.txt) if err : os.WriteFile(oldPath, []byte(hello), 0644); err ! nil { fmt.Println(write file:, err) return } if err : os.Rename(oldPath, newPath); err ! nil { fmt.Println(rename:, err) return } if err : os.Remove(newPath); err ! nil { fmt.Println(remove:, err) return } _, err os.Stat(newPath) fmt.Println(removed:, errors.Is(err, os.ErrNotExist)) }输出removed: true注意os.Remove 删除单个文件或空目录。 os.RemoveAll 会递归删除整个目录树。使用os.RemoveAll时要非常小心路径算错会删掉不该删的东西。十七、临时文件和临时目录写测试、生成中间文件、处理上传内容时经常需要临时文件。Go 提供os.CreateTemp os.MkdirTemp示例package main import ( fmt os ) func main() { file, err : os.CreateTemp(, report-*.txt) if err ! nil { fmt.Println(create temp file:, err) return } name : file.Name() defer os.Remove(name) defer file.Close() if _, err : file.WriteString(temporary report\n); err ! nil { fmt.Println(write temp file:, err) return } fmt.Println(temp file created:, name ! ) }输出temp file created: trueos.CreateTemp(, report-*.txt)里第一个参数是目录。传空字符串表示使用系统默认临时目录。第二个参数是文件名模式*会被替换成随机字符串。十八、关闭文件时的错误很多时候你会看到defer file.Close()这很常见也够用。但要知道Close 本身也可能返回错误。对于只读文件忽略Close错误通常问题不大。对于写文件如果你非常关心数据是否完整落盘就应该认真处理写入错误、Flush 错误、Close 错误。例如func writeText(path string, text string) (err error) { file, err : os.Create(path) if err ! nil { return fmt.Errorf(create file: %w, err) } defer func() { closeErr : file.Close() if err nil closeErr ! nil { err fmt.Errorf(close file: %w, closeErr) } }() if _, err : file.WriteString(text); err ! nil { return fmt.Errorf(write file: %w, err) } return nil }这段代码用了命名返回值func writeText(path string, text string) (err error)这样defer里可以在函数真正返回前补充关闭文件时的错误。新手阶段不用每段代码都写这么复杂但要知道关键写入场景不能永远无视Close的错误。十九、实战把文件里的空行去掉最后写一个小实战。需求读取 input.txt 去掉空行 把结果写入 output.txt完整代码package main import ( bufio fmt os path/filepath strings ) func removeEmptyLines(inputPath, outputPath string) error { input, err : os.Open(inputPath) if err ! nil { return fmt.Errorf(open input file: %w, err) } defer input.Close() output, err : os.Create(outputPath) if err ! nil { return fmt.Errorf(create output file: %w, err) } defer output.Close() scanner : bufio.NewScanner(input) writer : bufio.NewWriter(output) for scanner.Scan() { line : scanner.Text() if strings.TrimSpace(line) { continue } if _, err : writer.WriteString(line \n); err ! nil { return fmt.Errorf(write output line: %w, err) } } if err : scanner.Err(); err ! nil { return fmt.Errorf(scan input file: %w, err) } if err : writer.Flush(); err ! nil { return fmt.Errorf(flush output file: %w, err) } return nil } func main() { dir, err : os.MkdirTemp(, go-file-demo-*) if err ! nil { fmt.Println(create temp dir:, err) return } defer os.RemoveAll(dir) inputPath : filepath.Join(dir, input.txt) outputPath : filepath.Join(dir, output.txt) content : apple\n\nbanana\n \norange\n if err : os.WriteFile(inputPath, []byte(content), 0644); err ! nil { fmt.Println(write input:, err) return } if err : removeEmptyLines(inputPath, outputPath); err ! nil { fmt.Println(remove empty lines:, err) return } data, err : os.ReadFile(outputPath) if err ! nil { fmt.Println(read output:, err) return } fmt.Print(string(data)) }输出apple banana orange这个例子串起了很多文件处理知识os.Open打开输入文件os.Create创建输出文件defer Close关闭文件bufio.Scanner按行读取strings.TrimSpace判断空行bufio.Writer缓冲写入writer.Flush把缓冲内容写入文件使用%w保留底层错误

相关新闻