Go 文件与 IO 操作知识点总结:操作系统底层、源码原理与 bufio 流式高性能实战
Go 文件与 IO 操作 完整精讲
文章目录
- Go 文件与 IO 操作 完整精讲
- 一、先理清三大包定位(面试必区分)
- 二、底层前置知识:操作系统 IO 基础
- 文件描述符 File Descriptor(fd)
- 两种 IO 模式
- 三、io 包核心接口(抽象层,重中之重)
- 1. io.ReadAll () 一次性读取全部数据
- 2. io.Copy(dst Writer, src Reader)
- 3. io.CopyN(dst Writer, src Reader, n int64)
- 4. io.LimitReader(r Reader, n int64)
- 5. io.MultiReader 拼接多个 Reader
- 6. io.MultiWriter 多目的地同时写入
- 7. io.TeeReader(r Reader, w Writer)
- 四、os 包:文件基础操作 + 路径操作
- 一、文件操作(最高频)
- 1. os.Open () 只读打开文件
- 2. os.Create () 创建 / 覆写文件
- 3. os.OpenFile () 万能打开函数(底层,Open/Create 都是封装它)
- flag 常用常量(可组合,`|` 连接)
- perm 文件权限(Linux)
- 4. File 对象读写方法
- 5. 文件删除、重命名
- 二、文件夹操作
- 三、文件信息查询
- 四、环境变量相关
- 五、进程标准流(Stdin/Stdout/Stderr)
- 六、进程相关
- 七、路径工具(搭配 os 使用,更多路径操作推荐 path /filepath 包)
- ⚠️ 高频易错坑
- 基础读写示例
- 1. 常用 flag 组合(背诵)
- 2. 易错坑点
- 3. 使用场景建议
- 五、bufio 缓冲 IO:高效流式处理(考点核心)
- 底层原理
- bufio.Reader 读流程
- bufio.Writer 写流程
- 常用代码示例
- 高频函数
- `bufio.Scanner`:最适合**按行遍历大文件**
- 一、基础创建
- 二、核心执行流程(固定模板)
- 三、常用方法清单
- 1. `Scan() bool`
- 2. `Text() string`
- 3. `Bytes() []byte`
- 4. `Err() error`
- 5.`Split(split SplitFunc)`
- 6. `Buffer(buf []byte, max int)`
- 7. `MaxScanTokenSize`
- 四、完整可运行示例
- 五、面试对比:Scanner VS bufio.Reader ReadLine / ReadString
- 六、高频坑(必背)
- 📝极简背诵总结
- 2.bufio.NewReader/bufio.NewWriter 常用方法汇总(面试速记)
- 一、bufio.Reader(NewReader)
- 核心方法
- 记忆重点
- 二、bufio.Writer(NewWriter)
- 核心方法
- 致命坑(必背)
- 极简对比速查表
- 六、三种读写方案对比(选型标准)
- 七、底层深入:os.File 源码结构(简化)
- bufio.Reader 简化结构
- 八、关键坑点(面试高频)
- 九、完整流式大文件 Demo(贴合题干「高效流式处理」)
- 十、知识点总结背诵版
核心包:
os/io/bufio目标:文件读写、路径操作、高效流式处理,附带操作系统底层原理 + Go 源码层面讲解
一、先理清三大包定位(面试必区分)
os:操作系统资源封装- 直接操作文件、目录、进程、权限;
os.File代表操作系统打开的文件描述符 (fd);- 提供路径、文件创建、删除、文件元信息查询。
io:基础抽象接口(整个 Go IO 体系基石)定义标准接口:
ReaderWriterCloserSeeker所有可读对象只要实现
Read(p []byte)(n int, err error)就是 Reader,实现解耦。bufio:缓冲 IO,流式高效读写在 os.File 之上增加内存缓冲区,减少系统调用次数,优化 IO 性能。
二、底层前置知识:操作系统 IO 基础
文件描述符 File Descriptor(fd)
进程打开文件时,操作系统内核返回一个非负整数 fd;
os.File内部封装了 fd,所有读写操作最终都会调用 Linux 系统调用:
open/read/write/close。
两种 IO 模式
无缓冲 IO(os.File 直接 Read/Write)
每次调用 Read → 直接发起一次系统调用,频繁读写小数据时开销巨大。
用户态缓冲 IO(bufio)
在应用内存开辟一块缓冲区,程序先读写缓冲区;缓冲区满 / 空时,才向内核发起系统调用,大幅减少系统调用次数。
系统调用需要 CPU 从用户态切换内核态,切换成本很高,这就是 bufio 性能优势根源。
三、io 包核心接口(抽象层,重中之重)
// 基础读接口 type Reader interface { Read(p []byte) (n int, err error) } // 基础写接口 type Writer interface { Write(p []byte) (n int, err error) } // 关闭资源 type Closer interface { Close() error } // 偏移读写(文件支持,管道/网络流不支持) type Seeker interface { Seek(offset int64, whence int) (int64, error) }✅os.File同时实现了Reader/Writer/Closer/Seeker。
工具函数io.Copy(dst Writer, src Reader)
不需要手动开辟缓冲区,自动把源数据拷贝到目标,通用流式拷贝。
1. io.ReadAll () 一次性读取全部数据
作用:循环读取 Reader 直到 EOF,返回全部 [] byte
✅适用:小数据
❌禁止:超大文件、无限数据流(会爆内存)
r := bytes.NewBufferString("read all demo") data, err := io.ReadAll(r) if err != nil { panic(err) } fmt.Println(string(data))2. io.Copy(dst Writer, src Reader)
作用:从 src 读取,写入 dst;内置 32KB 缓冲,不用手动循环 Read
重点:Copy不会自动关闭 src /dst,文件、网络连接需要手动 Close!
src := bytes.NewBufferString("copy test") var dst bytes.Buffer _, err := io.Copy(&dst, src) if err != nil { panic(err) } fmt.Println(dst.String())3. io.CopyN(dst Writer, src Reader, n int64)
只复制最多 N 个字节,超过部分直接丢弃
// 只拷贝前5个字节 io.CopyN(&dst, src, 5)4. io.LimitReader(r Reader, n int64)
包装 Reader,限制最多读取 n 字节,常用于防攻击(限制请求 body 最大大小)
r := bytes.NewBufferString("123456789") limitR := io.LimitReader(r, 4) // 最多读4字节 data, _ := io.ReadAll(limitR) fmt.Println(string(data)) // 输出:12345. io.MultiReader 拼接多个 Reader
按顺序读取多个数据源,读完第一个自动读取第二个
r1 := bytes.NewBufferString("a") r2 := bytes.NewBufferString("b") rAll := io.MultiReader(r1, r2) data, _ := io.ReadAll(rAll) fmt.Println(string(data)) // ab6. io.MultiWriter 多目的地同时写入
一份数据,同时写入多个 Writer(类似日志同时输出到控制台 + 文件)
var buf1, buf2 bytes.Buffer w := io.MultiWriter(&buf1, &buf2) _, _ = w.Write([]byte("hello")) fmt.Println(buf1.String(), buf2.String())7. io.TeeReader(r Reader, w Writer)
一边读取源数据,一边自动复制一份写入 w;实现「读取同时备份」
src := bytes.NewBufferString("content") var backup bytes.Buffer teeR := io.TeeReader(src, &backup) data, _ := io.ReadAll(teeR) fmt.Println("读到的数据:", string(data)) fmt.Println("备份缓冲区:", backup.String())四、os 包:文件基础操作 + 路径操作
一、文件操作(最高频)
1. os.Open () 只读打开文件
func Open(name string) (*File, error)特性:只读模式,不能写入;文件不存在返回错误。
file, err := os.Open("test.txt") if err != nil { panic(err) } defer file.Close() // 必须关闭,防止文件句柄泄漏 // 搭配io.ReadAll直接读取全部内容 data, err := io.ReadAll(file) fmt.Println(string(data))2. os.Create () 创建 / 覆写文件
func Create(name string) (*File, error)规则:
- 文件不存在 → 创建;
- 文件已存在 →清空原有内容(截断);
- 权限:
0666(rw-rw-rw);读写模式。
file, err := os.Create("out.txt") defer file.Close() file.Write([]byte("hello os"))3. os.OpenFile () 万能打开函数(底层,Open/Create 都是封装它)
简易封装:
os.Open(path)→ O_RDONLY 只读打开os.Create(path)→ O_RDWR|O_CREATE|O_TRUNC 创建 / 清空文件
func OpenFile(name string, flag int, perm FileMode) (*File, error)flag 常用常量(可组合,|连接)
| 常量 | 含义 |
|---|---|
os.O_RDONLY | 只读 |
os.O_WRONLY | 只写 |
os.O_RDWR | 读写 |
os.O_APPEND | 追加写入,不覆盖旧数据 |
os.O_CREATE | 不存在则创建 |
os.O_TRUNC | 存在就清空截断 |
perm 文件权限(Linux)
0644:所有者读写,其他人只读(最常用)0755:可执行文件权限
示例:追加写文件(不会覆盖)
file, err := os.OpenFile("log.txt", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) defer file.Close() file.Write([]byte("追加一行日志\n"))4. File 对象读写方法
// 实现 io.Reader n, err := file.Read(buf []byte) // 实现 io.Writer n, err := file.Write([]byte) n, err := file.WriteString("文本") // 文件指针跳转(实现 io.Seeker) offset, err := file.Seek(10, io.SeekStart)5. 文件删除、重命名
// 删除文件 err := os.Remove("test.txt") // 重命名/移动文件 err := os.Rename("old.txt", "new.txt") // RemoveAll:递归删除文件夹+内部所有内容(rm -rf) err := os.RemoveAll("./tmpdir")二、文件夹操作
// 创建单层目录 err := os.Mkdir("mydir", 0755) // MkdirAll:递归创建多级目录(mkdir -p) err := os.MkdirAll("a/b/c", 0755) // 读取目录下所有文件信息 entries, err := os.ReadDir("./") for _, entry := range entries { fmt.Println(entry.Name(), entry.IsDir()) }三、文件信息查询
// 获取文件元信息(大小、修改时间、是否目录) info, err := os.Stat("test.txt") fmt.Println("大小:", info.Size()) fmt.Println("是否目录:", info.IsDir()) fmt.Println("修改时间:", info.ModTime()) // os.Lstat():和Stat类似,但不跟随软链接 info, err := os.Lstat("link.txt")四、环境变量相关
// 获取环境变量,不存在返回空字符串 val := os.Getenv("PATH") // 设置环境变量 err := os.Setenv("TEST_KEY", "123") // 查询环境变量是否存在 val, ok := os.LookupEnv("TEST_KEY") // 清除环境变量 os.Unsetenv("TEST_KEY") // 获取全部环境变量 envs := os.Environ()五、进程标准流(Stdin/Stdout/Stderr)
os.Stdin // 标准输入(终端输入)*os.File os.Stdout // 标准输出 fmt.Println底层写入这里 os.Stderr // 标准错误输出示例:直接往控制台输出
os.Stdout.WriteString("打印到终端\n")六、进程相关
// 获取当前进程PID pid := os.Getpid() // 获取父进程PID ppid := os.Getppid() // 退出程序;非0代表异常退出 os.Exit(1)七、路径工具(搭配 os 使用,更多路径操作推荐 path /filepath 包)
filepath:跨平台路径处理(Windows / Linux 分隔符自动适配,优先使用)
常用 API:
filepath.Join(a,b,c):拼接路径,自动处理分隔符filepath.Abs(path):获取绝对路径filepath.Base(path):获取文件名filepath.Dir(path):获取目录filepath.Glob("*.txt"):通配符匹配文件
// 判断文件是否存在(易错!不能用err==nil直接判断) func FileExists(path string) bool { _, err := os.Stat(path) return !os.IsNotExist(err) } // 判断错误是否为「文件不存在」 os.IsNotExist(err) // 判断是否权限不足 os.IsPermission(err)⚠️ 高频易错坑
- 打开文件必须 defer Close (),长期运行服务会耗尽文件句柄;
os.Create()打开已有文件直接清空!追加场景必须使用OpenFile + O_APPEND;os.Mkdir只能创建单层目录,多级目录请用MkdirAll;os.File实现io.Reader,所以可以直接传入io.Copy、io.ReadAll;- Windows 和 Linux 文件路径分隔符不一样,路径拼接优先使用
filepath.Join()。
基础读写示例
package main import ( "os" ) func main() { // 只读打开 f, err := os.Open("test.txt") if err != nil {panic(err)} defer f.Close() // 必须关闭,释放文件描述符! buf := make([]byte, 1024) n, err := f.Read(buf) // 直接无缓冲读取 _ = n }⚠️ 重要规范:打开文件一定要 defer Close ()
底层:进程不 close,fd 不会释放;大量泄漏会触发「打开文件数过多」崩溃。
1. 常用 flag 组合(背诵)
// 等价 os.Open(只读打开) f, _ := os.OpenFile("a.txt", os.O_RDONLY, 0) // 等价 os.Create(读写,不存在创建,存在清空) // O_RDWR | O_CREATE | O_TRUNC f, _ := os.OpenFile("a.txt", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) // 追加写入(文件尾部添加,不清空旧内容) f, _ := os.OpenFile("a.txt", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) 数字含义拆分 权限格式:0 [所有者][同组用户][其他用户] 0 6 4 4 第一位 0:八进制前缀,无权限含义 第二位 6 → 文件拥有者(user) 第三位 4 → 同组用户(group) 第四位 4 → 外部其他用户(other) 单个数字 = 读 + 写 + 执行权限之和: 4 = 读权限 r 2 = 写权限 w 1 = 执行权限 x 拆解 0644 所有者 6 = 4+2 → r w - 可读、可写,不可执行 同组用户 4 = 4 → r - - 仅可读 其他用户 4 = 4 → r - - 仅可读 字符表示:-rw-r--r--2. 易错坑点
- os.Open 拿到的文件句柄不能 Write ()
f, _ := os.Open("test.txt") f.Write([]byte("hello")) // ❌ 报错,只读文件无法写入perm 参数仅新建文件时生效
文件已经存在时,perm 参数会被直接忽略,不会修改已有文件权限。
想要修改已有文件权限,单独调用
os.Chmod()。err = os.Chmod(fileName, 0644) if err != nil { fmt.Println("修改权限失败:", err) return } fmt.Println("权限修改成功,设置为 0644 (-rw-r--r--)")flag 三大基础模式三选一,不能同时写多个
O_RDONLY只读O_WRONLY只写O_RDWR读写
错误示范:
O_RDONLY | O_WRONLY非法!
3. 使用场景建议
- 只读取文件内容→ 直接用
os.Open(简洁) - 需要写入、创建、追加、覆盖文件→ 必须使用
os.OpenFile
五、bufio 缓冲 IO:高效流式处理(考点核心)
底层原理
bufio.Reader/bufio.Writer内部维护一块[]byte缓冲区。
bufio.Reader 读流程
- 用户调用 Read → 先尝试从内存缓冲区拿数据;
- 缓冲区空 → 调用底层 os.File.Read,一次性读取一大块数据填满缓冲区;
- 后续读取继续复用缓冲区,减少系统调用。
bufio.Writer 写流程
- Write 数据先写入内存缓冲区;
- 缓冲区满,自动刷入底层文件;
- 文件关闭前必须调用 Flush (),把缓冲区残留数据写入磁盘!
常用代码示例
package main import ( "bufio" "os" ) func main() { f, _ := os.OpenFile("test.txt", os.O_RDWR|os.O_CREATE, 0644) defer f.Close() // 带缓冲写入 w := bufio.NewWriter(f) w.WriteString("hello go io\n") w.Flush() // 强制刷盘,不可省略! // 带缓冲读取 r := bufio.NewReader(f) // ReadString('\n') 按行读取,流式处理大文件首选 line, _ := r.ReadString('\n') _ = line }高频函数
bufio.Scanner:最适合按行遍历大文件
scanner := bufio.NewScanner(f) for scanner.Scan() { line := scanner.Text() // 逐行处理,不需要一次性把整个文件加载进内存(流式) }优势:处理 GB 级大文件不会 OOM,流式处理,题干重点要求。
一、基础创建
// 从io.Reader创建(文件、strings.Reader都可以) scanner := bufio.NewScanner(r)二、核心执行流程(固定模板)
scanner := bufio.NewScanner(reader) // 循环扫描,直到读到末尾或者出错 for scanner.Scan() { // 获取当前扫描到的文本 line := scanner.Text() fmt.Println(line) } // ❗必须检查扫描过程中的错误(不能忘!) if err := scanner.Err(); err != nil { fmt.Println("读取失败:", err) }三、常用方法清单
1.Scan() bool
- 作用:向前读取下一段数据
- 返回值:
true:成功读到一段内容,可以调用 Text ()/Bytes () 取值false:读取结束(EOF)或者发生错误
⚠️ 注意:Scan 不会返回错误,错误统一在循环结束后用
scanner.Err()获取
2.Text() string
获取本次扫描内容,返回string。底层是把字节切片转为字符串。
3.Bytes() []byte
获取本次扫描内容,返回[]byte。
✅ 性能更好,注意:下一次调用 Scan () 会覆盖这个切片内容!不要异步保存!
4.Err() error
获取扫描期间产生的错误(排除 io.EOF,EOF 不算错误)
循环结束一定要调用,防止漏掉读写异常。
5.Split(split SplitFunc)
⭐重点:自定义分割函数,控制 Scanner 按什么规则切割数据
内置 4 种分割器(直接背)
// 1. bufio.ScanLines 默认!按行分割,自动去掉 \n \r\n scanner.Split(bufio.ScanLines) // 2. bufio.ScanWords 按空格分割单词 scanner.Split(bufio.ScanWords) // 3. bufio.ScanBytes 逐个字节读取 scanner.Split(bufio.ScanBytes) // 4. bufio.ScanRunes 按utf8字符读取 scanner.Split(bufio.ScanRunes)6.Buffer(buf []byte, max int)
设置缓冲区,解决单行超过 64KB 默认上限报错
默认限制:单行最大 64KB,超长直接返回错误!
// 设置最大支持 1MB 的单行 buf := make([]byte, 1024*1024) scanner.Buffer(buf, 1024*1024)7.MaxScanTokenSize
常量:默认最大扫描长度 64 * 1024 = 65536
四、完整可运行示例
package main import ( "bufio" "fmt" "strings" ) func main() { data := strings.NewReader("第一行\n第二行\r\n第三行") sc := bufio.NewScanner(data) // 默认分割:ScanLines for sc.Scan() { str := sc.Text() fmt.Printf("行内容:%q\n", str) } // 检查错误 if err := sc.Err(); err != nil { fmt.Println("err:", err) } }五、面试对比:Scanner VS bufio.Reader ReadLine / ReadString
bufio.Scanner ✅推荐业务使用
- API 简洁,自动处理换行
\n \r\n - 不用手动处理截断、缓冲区碎片
- 唯一缺点:单行默认上限 64KB,超长要手动扩容
- API 简洁,自动处理换行
bufio.Reader ReadLine ❌不推荐
底层原始 API,容易出现超长行截断 bug,需要自己循环拼接碎片
bufio.Reader ReadString(‘\n’)
可控性更强,适合精细 IO 场景,适合学习底层原理
六、高频坑(必背)
循环结束必须判断
scanner.Err(),很多人漏掉!scanner.Bytes()返回的切片会被下一次 Scan 覆盖,如果需要保存要手动拷贝
// 错误:直接保存,下次Scan内容会变 tmp := scanner.Bytes() // 正确:拷贝 tmp := append([]byte(nil), scanner.Bytes()...)单行超过 64KB 直接报错,大日志文件要调用
Buffer()扩容
📝极简背诵总结
bufio.Scanner是封装好的扫描工具,Scan()推进读取,Text()/Bytes()获取内容;支持自定义分割规则;默认按行切割,单行上限 64KB;循环结束务必检查 Err ()。
2.bufio.NewReader/bufio.NewWriter 常用方法汇总(面试速记)
前置:
// 读缓冲 r := bufio.NewReader(io.Reader) // 写缓冲 w := bufio.NewWriter(io.Writer)
一、bufio.Reader(NewReader)
核心方法
ReadString(delim byte) (string, error)
读到分隔符停止,返回内容包含分隔符。
示例:
r.ReadString('\n')按行读取。读到文件末尾无分隔符时,返回读到的数据 +
io.EOF。ReadBytes(delim byte) ([]byte, error)
功能和 ReadString 一致,返回
[]byte而非字符串。ReadLine() ([]byte, bool, error)
按行读取,丢弃换行符
\n/\r\n;官方不推荐优先使用,存在缓冲区截断问题。Read(p []byte) (n int, err error)
标准 io.Reader 接口方法,填充字节切片。
Peek(n int) ([]byte, error)
预读 n 个字节,不移动读取指针,只偷看不消费。高频考点。
Discard(n int) (discarded int, err error)
直接跳过 n 个字节,丢弃数据。
记忆重点
✅ 按行优先:ReadString('\n')
✅ 偷看数据:Peek()
❌ 少用 ReadLine
二、bufio.Writer(NewWriter)
核心方法
Write(p []byte) (n int, err error)
写入字节切片,数据存入缓冲区,不直接落盘。
WriteString(s string) (n int, err error)
写入字符串,优先使用,省去 string→[] byte 转换开销。
Flush () error 【重中之重】
强制把缓冲区所有缓存数据写入底层流。
⚠️ 忘记 Flush → 缓冲区残留数据丢失!
Buffered() int
查询当前缓冲区暂存的字节数量。
Available() int
缓冲区剩余空闲空间大小。
致命坑(必背)
defer 文件.Close()不会自动调用 Flush ()
标准顺序:
w.Flush() f.Close()极简对比速查表
| 对象 | 高频方法 | 核心考点 |
|---|---|---|
| Reader | ReadString、Peek、ReadBytes | ReadString 带回分隔符;Peek 不移动指针 |
| Writer | WriteString、Flush、Buffered | 必须 Flush,否则丢数据 |
六、三种读写方案对比(选型标准)
os.File 直接 Read/Write(无缓冲)
适用:单次读写大块数据;不适合频繁小 IO。
缺点:小循环读写,大量系统调用,性能差。
bufio 缓冲读写
适用:频繁小数据读写、按行读取大文件、流式处理。
优点:合并 IO 请求,减少用户态 / 内核态切换,性能高。
os.ReadFile / os.WriteFile
os.ReadFile("a.txt") // 一次性把整个文件读入内存⚠️致命限制:一次性加载全部内容到内存!
禁止用于超大文件,极易内存溢出,不属于流式处理。
七、底层深入:os.File 源码结构(简化)
// src/os/file.go type File struct { *file // 指针 } type file struct { fd int // 文件描述符 name string dirinfo *dirInfo }所有f.Read()最终调用平台相关系统调用:
Linux:syscall.Read(fd, buf)
bufio.Reader 简化结构
type Reader struct { buf []byte // 缓冲区内存 rd io.Reader // 底层数据源(os.File) r, w int // 读写位置指针 }缓冲区默认大小 4096 字节,可以手动指定:bufio.NewReaderSize(f, 8192)
八、关键坑点(面试高频)
bufio.Writer 忘记 Flush ()
程序退出,缓冲区剩余数据丢失!
忘记 Close () 文件→ 文件描述符泄漏
os.ReadFile读取大文件造成 OOM,不能用于流式场景filepath和path包区别:path:只处理/分隔符(url 路径)filepath:适配操作系统,本地文件路径必须用它
权限参数
0644Linux 文件权限:读写权限,创建文件时必须指定。
九、完整流式大文件 Demo(贴合题干「高效流式处理」)
package main import ( "bufio" "fmt" "os" ) func main() { f, err := os.Open("bigfile.log") if err != nil { panic(err) } defer f.Close() scanner := bufio.NewScanner(f) // 逐行流式读取,内存只保存一行数据 lineNo := 0 for scanner.Scan() { lineNo++ content := scanner.Text() fmt.Printf("行%d:%s\n", lineNo, content) } // 检查扫描是否出现错误 if err := scanner.Err(); err != nil { panic(err) } }特点:无论文件 1GB 还是 10GB,内存占用稳定,典型流式 IO。
十、知识点总结背诵版
io包定义Reader/Writer抽象接口,统一所有可读写资源;os包封装操作系统文件、fd、路径、目录操作;bufio在用户态提供缓冲区,减少系统调用,实现高效 IO;- 无缓冲 IO每次读写触发系统调用;缓冲 IO 批量合并请求;
- 超大文件处理使用
bufio.Scanner流式逐行读取,避免一次性载入内存; - 文件打开务必
defer Close();bufio 写入结束必须Flush()。
