Go语言文件操作IO保姆级指南:从打开到避坑,一篇全搞定(附速查表)
阅读本文你将收获:彻底搞懂 Go 语言中的文件操作、IO 流概念、
os包与bufio包的使用方式,以及文件指针变化带来的各种“坑”。
目录
- Go语言文件操作IO保姆级指南:从打开到避坑,一篇全搞定(附速查表)
- 一、核心概念:文件与流
- 什么是"流"?
- 二、打开/创建文件:一切操作的起点
- 2.1 os.Create
- 2.2 os.Open
- 2.3 os.OpenFile
- Flag 标志位速查
- 2.4 Linux 文件权限速查
- 三、读取文件:四种方式全解析
- 3.1 file.Read —— 手动控制缓冲区
- 3.2 io.ReadAll —— 一次性读完
- 3.3 os.ReadFile —— 最简洁的方式
- 3.4 bufio.Reader —— 逐行读取
- 四、写入文件:三种姿势
- 4.1 file.Write / file.WriteString
- 4.2 bufio.Writer —— 带缓冲写入
- 4.3 os.WriteFile —— 一步到位
- 五、IO 接口:Go 的读写灵魂
- 5.1 io.Reader
- 5.2 io.Writer
- 5.3 io.Copy —— 拷贝就这么简单
- 六、文件指针:90% 的 Bug 来源
- 6.1 什么是文件指针?
- 6.2 file.Seek —— 手动操控指针
- 6.3 各操作对指针的影响表
- 6.4 经典踩坑场景
- 坑 1:ReadAll 之后再 Read —— 啥也读不到
- 坑 2:读取后写入会覆盖
- 坑 3:bufio.Writer 忘记 Flush
- 七、文件信息与目录操作
- 7.1 file.Stat —— 获取文件信息
- 7.2 目录操作
- 7.3 路径拼接
- 八、快速参考速查表
- 操作三合一速查
- 方案选择决策树
- 九、总结
- 三个核心概念
- 两条黄金法则
- 三个最常见的坑
一、核心概念:文件与流
在开始写代码之前,先理解一个关键概念:
文件在程序中是以"流"的形式来操作的。
什么是"流"?
流(Stream):数据在数据源(文件/网络/数据库)和程序内存之间经历的路径。
- 输入流(Input Stream):数据从数据源 → 程序内存(即"读取")
- 输出流(Output Stream):数据从程序内存 → 数据源(即"写入")
一句话总结:程序本身只操作内存,但文件在磁盘、数据库在服务器、网络数据在网卡——所以需要IO。
二、打开/创建文件:一切操作的起点
无论读还是写,第一步永远是获取文件指针(以下file均代表文件指针)。
2.1 os.Create
// 无则创建,有则清空(相当于 O_CREATE | O_TRUNC | O_WRONLY)file,err:=os.Create("test.txt")iferr!=nil{panic(err)}deferfile.Close()// 记得关闭!| 特性 | 说明 |
|---|---|
| 文件存在 | 清空原有内容 |
| 文件不存在 | 创建新文件 |
| 打开模式 | 只写 |
2.2 os.Open
// 以只读方式打开文件file,err:=os.Open("test.txt")iferr!=nil{panic(err)}deferfile.Close()| 特性 | 说明 |
|---|---|
| 文件存在 | 打开,只读 |
| 文件不存在 | 返回错误 |
| 打开模式 | 只读 |
2.3 os.OpenFile
最灵活的方式,可以精确控制打开行为:
// os.OpenFile(文件名, flag, 权限)file,err:=os.OpenFile("test.txt",os.O_APPEND|os.O_CREATE|os.O_WRONLY,0644)iferr!=nil{panic(err)}deferfile.Close()Flag 标志位速查
| 标志 | 含义 | 使用场景 |
|---|---|---|
O_RDONLY | 只读 | 读取配置文件 |
O_WRONLY | 只写 | 写入日志 |
O_RDWR | 读写 | 需要同时读写的场景 |
O_APPEND | 追加到文件末尾 | 写日志,防止覆盖 |
O_CREATE | 文件不存在则创建 | 确保文件一定存在 |
O_EXCL | 文件存在则报错 | 创建锁文件,防止重复创建 |
O_TRUNC | 打开时清空文件 | 覆盖写入 |
O_SYNC | 同步写入磁盘 | 重要数据,不容丢失 |
多个 flag 用
|分隔:os.O_CREATE | os.O_WRONLY
2.4 Linux 文件权限速查
os.OpenFile的第三个参数是 Linux 风格的数字权限(使用的是八进制)(r:读,w:写,x:执行):
| 数字 | 权限 |
|---|---|
| 0 | ---无权限 |
| 1 | --x执行 |
| 2 | -w-写 |
| 3 | -wx写+执行 |
| 4 | r--读 |
| 5 | r-x读+执行 |
| 6 | rw-读+写 |
| 7 | rwx读+写+执行 |
// 常见权限示例:os.OpenFile("file.txt",os.O_CREATE|os.O_WRONLY,0644)// rw-r--r--os.OpenFile("file.txt",os.O_CREATE|os.O_WRONLY,0755)// rwxr-xr-x记忆技巧:读=4,写=2,执行=1,三者相加就是权限数字。
6= 4+2 = 读+写。
三、读取文件:四种方式全解析
3.1 file.Read —— 手动控制缓冲区
file,err:=os.Open("test.txt")iferr!=nil{panic(err)}deferfile.Close()buf:=make([]byte,1024)// 创建 1024 字节的缓冲区n,err:=file.Read(buf)// n = 实际读取的字节数iferr!=nil{panic(err)}fmt.Println(string(buf[:n]))// 只输出有效数据(0 到 n-1)适用场景:处理大文件,需要精确控制每次读取的字节数。
可以通过控制buf的长度大小来控制每次读取的字节数
注意:一定要用
buf[:n]而不是buf,因为buf的尾部可能包含上次读取的残留数据!
3.2 io.ReadAll —— 一次性读完
import"io"file,_:=os.Open("test.txt")deferfile.Close()data,err:=io.ReadAll(file)// data 是 []byte 类型iferr!=nil{panic(err)}fmt.Println(string(data))// 需要 string() 转换适用场景:文件不大,想一次性全部读入内存。Go 1.16 之前用ioutil.ReadAll,现已移至io.ReadAll。
3.3 os.ReadFile —— 最简洁的方式
// 注意:直接传文件名,不需要先 Open!data,err:=os.ReadFile("test.txt")// data 是 []byte 类型iferr!=nil{panic(err)}fmt.Println(string(data))Go 1.16 新增,一行代码搞定读文件。内部自动完成打开→读取→关闭。
适用场景:快速读取小文件(配置文件、模板等)。
3.4 bufio.Reader —— 逐行读取
import("bufio""io")file,_:=os.Open("test.txt")deferfile.Close()reader:=bufio.NewReader(file)for{line,err:=reader.ReadString('\n')// 读到一个换行符为止// 或者 reader.ReadBytes('\n') —— 返回 []bytefmt.Print(line)iferr==io.EOF{// 到达文件末尾break}}| 方法 | 返回值 | 说明 |
|---|---|---|
ReadString('\n') | string | 读到指定分隔符为止 |
ReadBytes('\n') | []byte | 同上,返回字节切片 |
ReadLine() | []byte | 读一行(不推荐) |
四、写入文件:三种姿势
4.1 file.Write / file.WriteString
file,err:=os.Create("output.txt")// 有则清空,无则创建iferr!=nil{panic(err)}deferfile.Close()// 方式一:写 []byten,err:=file.Write([]byte("Hello, Go!\n"))// 方式二:直接写字符串n,err=file.WriteString("第二行内容\n")fmt.Printf("写入了 %d 字节\n",n)4.2 bufio.Writer —— 带缓冲写入
import"bufio"file,_:=os.Create("output.txt")deferfile.Close()writer:=bufio.NewWriter(file)// 创建带缓冲的 Writerwriter.WriteString("第一行\n")writer.WriteString("第二行\n")writer.WriteString("第三行\n")writer.Flush()// ⚠️ 必须调用!否则数据不会真正写入文件这是最常见的坑:
bufio.Writer先把数据写到内存缓冲区,只有调用Flush()才会真正刷到磁盘。忘记Flush()= 文件空空如也!
4.3 os.WriteFile —— 一步到位
data:=[]byte("Hello, Go!")err:=os.WriteFile("output.txt",data,0644)iferr!=nil{panic(err)}一行代码完成:打开 → 写入 → 关闭。Go 1.16 新增。
三种写入方式对比:
| 方式 | 使用场景 | 注意 |
|---|---|---|
file.Write | 需要多次写入、追加 | 需手动 OpenFile |
bufio.Writer | 频繁小量写入,减少系统调用 | 必须 Flush! |
os.WriteFile | 一次性写完,简单快捷 | 每次都会覆盖 |
五、IO 接口:Go 的读写灵魂
Go 语言的 IO 设计极其优雅——通过两个核心接口统一了所有读写操作:
5.1 io.Reader
typeReaderinterface{Read(p[]byte)(nint,errerror)}任何拥有Read方法的类型,都可以称为Reader。
funcreadAll(r io.Reader){buf:=make([]byte,1024)n,_:=r.Read(buf)fmt.Println(string(buf[:n]))}// 以下全部可以传入 readAll:readAll(os.Stdin)// 标准输入readAll(strings.NewReader("hello"))// 字符串readAll(bytes.NewBuffer([]byte("world")))// 字节缓冲区readAll(file)// 文件这就是 Go 接口的威力——函数不关心数据来自哪里,只关心它"可读"。
5.2 io.Writer
typeWriterinterface{Write(p[]byte)(nint,errerror)}任何拥有Write方法的类型,都可以称为Writer。
funcwriteData(w io.Writer,datastring){w.Write([]byte(data))}// 以下全部可以传入 writeData:writeData(os.Stdout,"hello")// 输出到控制台writeData(file,"hello")// 输出到文件writeData(&bytes.Buffer{},"hello")// 输出到内存缓冲区5.3 io.Copy —— 拷贝就这么简单
// 将 src 的内容复制到 dstwritten,err:=io.Copy(dst Writer,src Reader)// 实际应用:复制文件srcFile,_:=os.Open("source.txt")defersrcFile.Close()dstFile,_:=os.Create("copy.txt")deferdstFile.Close()io.Copy(dstFile,srcFile)// 一行搞定文件复制!io.Copy内部使用 32KB 的缓冲区,循环读取并写入,直到io.EOF。
六、文件指针:90% 的 Bug 来源
6.1 什么是文件指针?
操作系统为每个打开的文件维护一个文件指针(offset),标记"当前读/写到哪个位置了"。
文件内容:H e l l o W o r l d 位置: 0 1 2 3 4 5 6 7 8 9 10 指针=0:从 'H' 开始读 指针=6:从 'W' 开始读 指针=11:在文件末尾,读取返回 io.EOF6.2 file.Seek —— 手动操控指针
file.Seek(offsetint64,whenceint)(int64,error)| whence 常量 | 起始位置 | 说明 |
|---|---|---|
io.SeekStart | 文件开头 | offset ≥ 0 |
io.SeekCurrent | 当前指针位置 | offset 可正可负 |
io.SeekEnd | 文件末尾 | offset ≤ 0 |
// 回到文件开头file.Seek(0,io.SeekStart)// 跳过前 10 个字节file.Seek(10,io.SeekStart)// 从当前位置向前移动 5 字节file.Seek(5,io.SeekCurrent)// 定位到文件末尾(常用于追加前获取文件大小)size,_:=file.Seek(0,io.SeekEnd)6.3 各操作对指针的影响表
| 操作 | offset 变化方式 |
|---|---|
File.Read() | 向后移动读取的字节数 |
io.ReadAll() | 移动到文件末尾 |
bufio.Reader.Read() | 底层读取,offset 随之移动 |
File.Write() | 向后移动写入的字节数 |
WriteString() | 向后移动字符串的字节数 |
bufio.Writer.Write() | 写入缓冲区,不立即移动;Flush 后才移动 |
Flush() | 实际写入多少,offset 移动多少 |
Seek() | 直接设置到指定位置 |
O_APPEND写入 | 写入前自动跳到文件末尾 |
O_TRUNC打开文件 | 清空内容,offset = 0 |
6.4 经典踩坑场景
坑 1:ReadAll 之后再 Read —— 啥也读不到
file,_:=os.Open("test.txt")data,_:=io.ReadAll(file)// 指针已到末尾fmt.Println(string(data))// 正确 有数据buf:=make([]byte,10)n,_:=file.Read(buf)// 错误!!! n=0,指针已在末尾!fmt.Println(n)// 输出:0解决:读完要再读,先file.Seek(0, io.SeekStart)回到开头。
坑 2:读取后写入会覆盖
// 文件原内容:"Hello World"(长度 11)file,_:=os.OpenFile("test.txt",os.O_RDWR,0644)deferfile.Close()buf:=make([]byte,6)file.Read(buf)// 读取 "Hello "(6 字节),指针=6file.WriteString("Go")// 从指针位置覆盖写入!指针=9结果:文件变成"Hello Gorld"——"Go"覆盖了"Wo"!
原文件:H e l l o W o r l d 操作: 读取前6位→ 指针到6→ 写入"Go"覆盖位置6和7 结果: H e l l o G o r l d解决:如果要在文件末尾写,用file.Seek(0, io.SeekEnd)或O_APPEND。
坑 3:bufio.Writer 忘记 Flush
file,_:=os.Create("test.txt")writer:=bufio.NewWriter(file)writer.WriteString("重要数据!!!")// 忘记 writer.Flush()file.Close()// Close 也不会帮你 Flush bufio.Writer结果:文件是空的!
解决:defer writer.Flush()是好习惯,但Flush返回的error也要检查:
deferfunc(){iferr:=writer.Flush();err!=nil{panic(err)}file.Close()}()七、文件信息与目录操作
7.1 file.Stat —— 获取文件信息
info,err:=file.Stat()// 或 os.Stat("文件名")iferr!=nil{panic(err)}fmt.Println("文件名:",info.Name())// eg:test.txtfmt.Println("文件大小:",info.Size())// int64,单位:字节fmt.Println("权限:",info.Mode())// -rw-r--r--fmt.Println("修改时间:",info.ModTime())// 最后修改时间fmt.Println("是目录:",info.IsDir())// true / false7.2 目录操作
// 重命名文件/目录err:=os.Rename("old_name.txt","new_name.txt")// 删除文件或空目录err:=os.Remove("file.txt")// 删除非空目录(递归删除)err:=os.RemoveAll("directory/")// 创建单层目录err:=os.Mkdir("newdir",0755)// 递归创建多级目录(类似 mkdir -p)err:=os.MkdirAll("a/b/c/d",0755)// 自动创建 a → a/b → a/b/c → a/b/c/d// 读取目录内容(不包含子目录的文件)files,err:=os.ReadDir(".")for_,f:=rangefiles{fmt.Println(f.Name())// 文件名fmt.Println(f.IsDir())// 是否是目录}注意:
os.Remove删除非空目录会失败,要用os.RemoveAll。
7.3 路径拼接
import"path/filepath"// 跨平台路径拼接path:=filepath.Join("a","b","c.txt")// Windows: a\b\c.txt// Linux: a/b/c.txt// 也支持多级拼接fullPath:=filepath.Join("/home","user","projects","main.go")注意:不要手动拼路径字符串(
"a/" + "b"),用filepath.Join自动处理跨平台分隔符。
八、快速参考速查表
操作三合一速查
| 要做什么 | 打开文件 | 读取/写入 | 关闭 |
|---|---|---|---|
| 一行读 | 不需要 | os.ReadFile(name) | 自动关闭 |
| 一行写 | 不需要 | os.WriteFile(name,data,perm) | 自动关闭 |
| 逐行读 | os.Open(name) | bufio.NewReader(f).ReadString('\n') | defer f.Close() |
| 追加写 | os.OpenFile(name, O_APPEND|O_CREATE|O_WRONLY, 0644) | f.WriteString(s) | defer f.Close() |
| 拷贝文件 | os.Open(src)+os.Create(dst) | io.Copy(dstFile, srcFile) | defer都关 |
方案选择决策树
读取文件 ├── 文件很小? → os.ReadFile() ├── 需要逐行处理? → bufio.NewReader + ReadString('\n') ├── 需要控制每次读取大小? → file.Read(buf) └── 所有内容一次性处理? → io.ReadAll(file) 写入文件 ├── 一次性全部写入? → os.WriteFile() ├── 需要多次写入? → file.Write / file.WriteString ├── 频繁小量写入? → bufio.NewWriter + Flush() └── 追加到末尾? → OpenFile(..., O_APPEND, ...)九、总结
三个核心概念
- 流(Stream):数据在数据源和程序内存之间的通道——输入流=读,输出流=写
- 文件指针(Offset):操作系统帮你维护的"当前读写位置",是大多数 Bug 的根源
- io.Reader / io.Writer:Go 的 IO 灵魂接口——所有"可读/可写"的东西都实现了它们
两条黄金法则
- 读取文件前必须先打开(
os.ReadFile除外,它帮你在内部做了) bufio.Writer必须Flush(),否则数据永远到不了磁盘
三个最常见的坑
| 坑 | 原因 | 解决 |
|---|---|---|
ReadAll后再Read啥也读不到 | 指针已在末尾 | Seek(0, io.SeekStart) |
| 读取部分内容后写入,把原内容覆盖了 | 指针在中间,写入会覆盖 | 先Seek到末尾或用O_APPEND |
| 程序跑了半天,打开文件一看啥也没有 | bufio.Writer忘了Flush | defer writer.Flush() |
本文涵盖了日常开发中最常用的文件操作场景。熟记文件指针的变化规律,就能避开 90% 的文件操作 Bug。
如果本文对你有帮助,欢迎点赞收藏