Go 标准库已经提供了完整工具。新手最常用的是这几个包:
os
io
bufio
path/filepath
errors
这篇文章会从最常见的场景开始,一步一步讲清楚 Go 文件处理。
先记住几个核心包
os
os 包负责和操作系统交互。
常用能力包括:
- 读文件:
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.MkdirTemp
io
io 包处理通用输入输出。
最重要的概念是:
io.Reader
io.Writer
很多东西都可以是 Reader 或 Writer:
- 文件
- 网络连接
- 字符串读取器
- bytes.Buffer
- HTTP 响应体
比如复制文件时常用:
io.Copy(dst, src)
bufio
bufio 包提供带缓冲的读写。
常见用途:
- 按行读取文件:
bufio.Scanner - 更高效地写入多行内容:
bufio.Writer
path/filepath
path/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: Charlie
scanner.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 data
io.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.Stat
os.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: 06
os.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: logs
os.ReadDir 只读取当前目录这一层。
如果你要递归遍历子目录,要用 filepath.WalkDir。
十三、递归遍历目录:filepath.WalkDir
filepath.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: .log
filepath.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: true
os.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保留底层错误