Go 文件与 IO 操作 完整精讲

文章目录


核心包: os / io / bufio

目标:文件读写、路径操作、高效流式处理,附带操作系统底层原理 + Go 源码层面讲解

一、先理清三大包定位(面试必区分)

  1. os :操作系统资源封装

    • 直接操作文件、目录、进程、权限;
    • os.File 代表操作系统打开的文件描述符 (fd)
    • 提供路径、文件创建、删除、文件元信息查询。
  2. io:基础抽象接口(整个 Go IO 体系基石)

    定义标准接口:

    Reader
    
    Writer
    
    Closer
    
    Seeker
    

    所有可读对象只要实现 Read(p []byte)(n int, err error) 就是 Reader,实现解耦。

  3. bufio :缓冲 IO,流式高效读写

    在 os.File 之上增加内存缓冲区,减少系统调用次数,优化 IO 性能。

二、底层前置知识:操作系统 IO 基础

文件描述符 File Descriptor(fd)

进程打开文件时,操作系统内核返回一个非负整数 fd;

os.File 内部封装了 fd,所有读写操作最终都会调用 Linux 系统调用:

open/read/write/close

两种 IO 模式

  1. 无缓冲 IO(os.File 直接 Read/Write)

    每次调用 Read → 直接发起一次系统调用,频繁读写小数据时开销巨大。

  2. 用户态缓冲 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)) // 输出:1234
5. io.MultiReader 拼接多个 Reader

按顺序读取多个数据源,读完第一个自动读取第二个

r1 := bytes.NewBufferString("a")
r2 := bytes.NewBufferString("b")
rAll := io.MultiReader(r1, r2)
data, _ := io.ReadAll(rAll)
fmt.Println(string(data)) // ab
6. 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)

规则:

  1. 文件不存在 → 创建;
  2. 文件已存在 → 清空原有内容(截断)
  3. 权限: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)

⚠️ 高频易错坑

  1. 打开文件必须 defer Close (),长期运行服务会耗尽文件句柄;
  2. os.Create() 打开已有文件直接清空!追加场景必须使用 OpenFile + O_APPEND
  3. os.Mkdir 只能创建单层目录,多级目录请用 MkdirAll
  4. os.File 实现 io.Reader,所以可以直接传入 io.Copy、io.ReadAll
  5. 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. 易错坑点

  1. os.Open 拿到的文件句柄不能 Write ()
f, _ := os.Open("test.txt")
f.Write([]byte("hello")) // ❌ 报错,只读文件无法写入
  1. perm 参数仅新建文件时生效

    文件已经存在时,perm 参数会被直接忽略,不会修改已有文件权限。

    想要修改已有文件权限,单独调用 os.Chmod()

    
    err = os.Chmod(fileName, 0644) 
    
    if err != nil { 	
    
    ​	fmt.Println("修改权限失败:", err) 	
    
    ​	return 
    
    } 
    
    fmt.Println("权限修改成功,设置为 0644 (-rw-r--r--)")
    
  2. 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 读流程
  1. 用户调用 Read → 先尝试从内存缓冲区拿数据;
  2. 缓冲区空 → 调用底层 os.File.Read,一次性读取一大块数据填满缓冲区;
  3. 后续读取继续复用缓冲区,减少系统调用。
bufio.Writer 写流程
  1. Write 数据先写入内存缓冲区;
  2. 缓冲区满,自动刷入底层文件;
  3. 文件关闭前必须调用 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
  1. bufio.Scanner ✅推荐业务使用

    • API 简洁,自动处理换行 \n \r\n
    • 不用手动处理截断、缓冲区碎片
    • 唯一缺点:单行默认上限 64KB,超长要手动扩容
  2. bufio.Reader ReadLine ❌不推荐

    底层原始 API,容易出现超长行截断 bug,需要自己循环拼接碎片

  3. bufio.Reader ReadString(‘\n’)

    可控性更强,适合精细 IO 场景,适合学习底层原理

六、高频坑(必背)
  1. 循环结束必须判断 scanner.Err(),很多人漏掉!

  2. scanner.Bytes()
    

    返回的切片会被下一次 Scan 覆盖,如果需要保存要手动拷贝

    // 错误:直接保存,下次Scan内容会变
    tmp := scanner.Bytes()
    // 正确:拷贝
    tmp := append([]byte(nil), scanner.Bytes()...)
    
  3. 单行超过 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)

核心方法
  1. ReadString(delim byte) (string, error)

    读到分隔符停止,返回内容包含分隔符

    示例:r.ReadString('\n') 按行读取。

    读到文件末尾无分隔符时,返回读到的数据 + io.EOF

  2. ReadBytes(delim byte) ([]byte, error)

    功能和 ReadString 一致,返回[]byte而非字符串。

  3. ReadLine() ([]byte, bool, error)

    按行读取,丢弃换行符\n/\r\n;官方不推荐优先使用,存在缓冲区截断问题。

  4. Read(p []byte) (n int, err error)

    标准 io.Reader 接口方法,填充字节切片。

  5. Peek(n int) ([]byte, error)

    预读 n 个字节,不移动读取指针,只偷看不消费。高频考点。

  6. Discard(n int) (discarded int, err error)

    直接跳过 n 个字节,丢弃数据。

记忆重点

✅ 按行优先:ReadString('\n')

✅ 偷看数据:Peek()

❌ 少用 ReadLine

二、bufio.Writer(NewWriter)

核心方法
  1. Write(p []byte) (n int, err error)

    写入字节切片,数据存入缓冲区,不直接落盘

  2. WriteString(s string) (n int, err error)

    写入字符串,优先使用,省去 string→[] byte 转换开销。

  3. Flush () error 【重中之重】

    强制把缓冲区所有缓存数据写入底层流。

⚠️ 忘记 Flush → 缓冲区残留数据丢失!

  1. Buffered() int

    查询当前缓冲区暂存的字节数量

  2. Available() int

    缓冲区剩余空闲空间大小。

致命坑(必背)

defer 文件.Close()不会自动调用 Flush ()

标准顺序:

w.Flush()
f.Close()
极简对比速查表
对象 高频方法 核心考点
Reader ReadString、Peek、ReadBytes ReadString 带回分隔符;Peek 不移动指针
Writer WriteString、Flush、Buffered 必须 Flush,否则丢数据

六、三种读写方案对比(选型标准)

  1. os.File 直接 Read/Write(无缓冲)

    适用:单次读写大块数据;不适合频繁小 IO。

    缺点:小循环读写,大量系统调用,性能差。

  2. bufio 缓冲读写

    适用:频繁小数据读写、按行读取大文件、流式处理。

    优点:合并 IO 请求,减少用户态 / 内核态切换,性能高。

  3. 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)

八、关键坑点(面试高频)

  1. bufio.Writer 忘记 Flush ()

    程序退出,缓冲区剩余数据丢失!

  2. 忘记 Close () 文件 → 文件描述符泄漏

  3. os.ReadFile 读取大文件造成 OOM,不能用于流式场景

  4. filepathpath包区别:

    • path:只处理 / 分隔符(url 路径)
    • filepath:适配操作系统,本地文件路径必须用它
  5. 权限参数

    0644
    

    Linux 文件权限:读写权限,创建文件时必须指定。

九、完整流式大文件 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。

十、知识点总结背诵版

  1. io 包定义 Reader/Writer 抽象接口,统一所有可读写资源;
  2. os 包封装操作系统文件、fd、路径、目录操作;
  3. bufio 在用户态提供缓冲区,减少系统调用,实现高效 IO;
  4. 无缓冲 IO每次读写触发系统调用;缓冲 IO 批量合并请求;
  5. 超大文件处理使用bufio.Scanner流式逐行读取,避免一次性载入内存;
  6. 文件打开务必defer Close();bufio 写入结束必须Flush()
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐