Go 从 0 到精通 · 第 19 课:文件操作基础
学习定位:这是整套 Go 教程的第 19 课,也是阶段四(标准库与实战阶段)的第一课。
前置要求:已经完成第 18 课,掌握了包与模块管理。同时需要熟悉第 16 课的 error 处理和第 17 课的 defer。
本课目标:掌握文件的创建、打开、读取、写入、删除,以及目录操作和路径处理,能编写简单但完整的文件处理程序。
1. 本课你要解决的核心问题
到目前为止,你写的程序都是从控制台输入、往控制台输出。但真实世界的程序几乎都要和文件打交道——读配置、写日志、处理数据、导出报告……
你需要搞明白以下问题:
- 怎么打开和关闭文件
- 怎么读取文件的全部内容或逐行读取
- 怎么写入文件、追加内容
- 怎么创建和删除文件、目录
- 怎么处理文件路径
os、io、bufio 这几个包各自干什么
学完这一课,你就能让程序和磁盘上的文件交互了。
2. 先搞清楚几个核心包
在 Go 中做文件操作,主要用到三个包:
| 包 |
职责 |
一句话概括 |
os |
操作系统接口 |
打开、创建、删除文件和目录 |
io |
I/O 原语 |
定义 Reader、Writer 接口 |
bufio |
带缓冲的 I/O |
逐行读取、高效写入 |
还有一小部分功能来自 path/filepath(路径操作)和 io/ioutil(在 Go 1.16 前常用的便捷函数,现在推荐用 os 和 io 替代)。
先有个整体印象,下面逐个展开。
3. 打开和关闭文件
3.1 用 os.Open 打开文件(只读)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package main
import ( "fmt" "os" )
func main() { file, err := os.Open("hello.txt") if err != nil { fmt.Println("打开文件失败:", err) return } defer file.Close()
fmt.Println("文件打开成功!") }
|
关键点:
os.Open 只能读,不能写
- 返回两个值:
*os.File 和 error
- 一定要调用
file.Close(),否则文件句柄泄漏
- 最佳实践:紧跟
open 之后写 defer file.Close()
3.2 用 os.OpenFile 打开文件(更灵活)
os.Open 其实是 os.OpenFile 的简化版。当你需要写入、追加、创建等操作时,用 os.OpenFile:
1 2 3 4 5 6
| file, err := os.OpenFile("data.txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { fmt.Println("打开文件失败:", err) return } defer file.Close()
|
os.OpenFile 接受三个参数:
- 文件路径
- 打开模式(flag)
- 权限(仅在创建文件时生效)
常用打开模式
| 模式 |
含义 |
os.O_RDONLY |
只读 |
os.O_WRONLY |
只写 |
os.O_RDWR |
读写 |
os.O_CREATE |
文件不存在则创建 |
os.O_TRUNC |
打开时清空内容 |
os.O_APPEND |
追加模式(写到文件末尾) |
这些模式可以用 | 组合使用。常见的组合:
1 2 3 4 5 6 7 8
| os.O_WRONLY | os.O_CREATE | os.O_TRUNC
os.O_WRONLY | os.O_CREATE | os.O_APPEND
os.O_RDWR | os.O_CREATE
|
文件权限参数
第三个参数 0644 是 Unix 风格的权限:
0644:所有者可读写,其他人只读(最常用)
0755:所有者可读写执行,其他人可读执行
0600:只有所有者可读写(最安全)
注意:在 Windows 上这个权限参数基本被忽略,但写上是好习惯,确保跨平台兼容。
3.3 完整的打开-关闭流程
1 2 3 4 5 6 7 8 9 10
| func processFile(path string) error { file, err := os.Open(path) if err != nil { return fmt.Errorf("无法打开文件 %s: %w", path, err) } defer file.Close()
return nil }
|
记住这个模式:检查错误 → defer 关闭 → 处理文件。后面几乎所有文件操作都遵循这个套路。
4. 读取文件
4.1 一次性读取全部内容
最简单的方式,适合小文件(几 MB 以内):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package main
import ( "fmt" "os" )
func main() { content, err := os.ReadFile("hello.txt") if err != nil { fmt.Println("读取失败:", err) return } fmt.Println(string(content)) }
|
os.ReadFile 返回 []byte,需要转成 string 才能直接打印。
它内部做了:打开文件 → 读取全部内容 → 关闭文件。你不需要手动管理文件句柄。
一个小例子:读取并统计行数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| package main
import ( "fmt" "os" )
func main() { content, err := os.ReadFile("poem.txt") if err != nil { fmt.Println("读取失败:", err) return }
text := string(content) lines := 0 for _, ch := range text { if ch == '\n' { lines++ } } if len(text) > 0 && text[len(text)-1] != '\n' { lines++ }
fmt.Printf("文件共有 %d 行\n", lines) }
|
4.2 逐块读取(适合大文件)
如果文件很大(比如几个 GB),一次性读入内存会把程序撑爆。这时要分块读取:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| package main
import ( "fmt" "os" )
func main() { file, err := os.Open("bigfile.txt") if err != nil { fmt.Println("打开失败:", err) return } defer file.Close()
buf := make([]byte, 1024) for { n, err := file.Read(buf) if n > 0 { fmt.Printf("读取了 %d 字节\n", n) } if err != nil { break } } }
|
file.Read(buf) 的行为:
- 返回两个值:实际读取的字节数
n 和 error
- 读到文件末尾时,
err 为 io.EOF
n 可能小于 len(buf)(最后一块通常不满)
这种手动分块读取的方式底层学习很有价值,但实际开发中我们更常用 bufio 来做,下面会讲。
4.3 用 bufio.Scanner 逐行读取(最常用)
处理文本文件时,最常见的需求是逐行读取。bufio.Scanner 是最佳选择:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| package main
import ( "bufio" "fmt" "os" )
func main() { file, err := os.Open("data.txt") if err != nil { fmt.Println("打开失败:", err) return } defer file.Close()
scanner := bufio.NewScanner(file) lineNum := 0 for scanner.Scan() { lineNum++ line := scanner.Text() fmt.Printf("第 %d 行: %s\n", lineNum, line) }
if err := scanner.Err(); err != nil { fmt.Println("读取出错:", err) } }
|
bufio.Scanner 的工作机制:
scanner.Scan():尝试读取下一行,成功返回 true,到达末尾或出错返回 false
scanner.Text():返回当前行的内容(string)
scanner.Bytes():返回当前行的内容([]byte)
一定要在循环结束后检查 scanner.Err(),它能告诉你循环是因为正常结束还是因为错误而退出的。
逐行读取的实际例子:过滤包含特定关键词的行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| package main
import ( "bufio" "fmt" "os" "strings" )
func main() { file, err := os.Open("server.log") if err != nil { fmt.Println("打开失败:", err) return } defer file.Close()
scanner := bufio.NewScanner(file) lineNum := 0 found := 0 for scanner.Scan() { lineNum++ line := scanner.Text() if strings.Contains(line, "ERROR") { found++ fmt.Printf("第 %d 行: %s\n", lineNum, line) } }
if err := scanner.Err(); err != nil { fmt.Println("读取出错:", err) return }
fmt.Printf("共找到 %d 行包含 ERROR\n", found) }
|
4.4 用 bufio.Reader 逐行读取
bufio.Reader 比 Scanner 更底层,功能更丰富:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| package main
import ( "bufio" "fmt" "io" "os" )
func main() { file, err := os.Open("data.txt") if err != nil { fmt.Println("打开失败:", err) return } defer file.Close()
reader := bufio.NewReader(file) lineNum := 0 for { line, err := reader.ReadString('\n') lineNum++ fmt.Printf("第 %d 行: %s", lineNum, line)
if err == io.EOF { break } if err != nil { fmt.Println("读取出错:", err) return } } }
|
Scanner vs Reader 怎么选:
|
bufio.Scanner |
bufio.Reader |
| 适合场景 |
逐行读取文本 |
更灵活的读取(按分隔符、按字节等) |
| API 简洁度 |
更简洁 |
稍复杂 |
| 单行长度限制 |
默认 64KB(可调整) |
无限制 |
| 推荐程度 |
日常首选 |
需要更多控制时使用 |
5. 写入文件
5.1 用 os.WriteFile 一次性写入
最简单的方式,一次性把内容写入文件(Go 1.16+):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package main
import ( "fmt" "os" )
func main() { data := []byte("Hello, Go 文件操作!\n这是第二行。")
err := os.WriteFile("output.txt", data, 0644) if err != nil { fmt.Println("写入失败:", err) return }
fmt.Println("写入成功!") }
|
os.WriteFile 的行为:
- 文件不存在 → 创建
- 文件已存在 → 清空后写入(覆盖)
- 内部自动打开和关闭文件
5.2 用 file.Write 和 file.WriteString 写入
当你需要分多次写入时,手动管理文件句柄:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| package main
import ( "fmt" "os" )
func main() { file, err := os.Create("output.txt") if err != nil { fmt.Println("创建失败:", err) return } defer file.Close()
_, err = file.Write([]byte("第一行内容\n")) if err != nil { fmt.Println("写入失败:", err) return }
_, err = file.WriteString("第二行内容\n") if err != nil { fmt.Println("写入失败:", err) return }
fmt.Println("写入成功!") }
|
os.Create 等价于 os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666),即:创建或截断文件。
5.3 追加写入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| package main
import ( "fmt" "os" )
func main() { file, err := os.OpenFile("log.txt", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) if err != nil { fmt.Println("打开失败:", err) return } defer file.Close()
file.WriteString("2024-01-15 10:00:00 用户登录\n") file.WriteString("2024-01-15 10:05:32 用户下单\n") file.WriteString("2024-01-15 10:06:01 支付成功\n")
fmt.Println("日志写入成功!") }
|
运行多次这个程序,你会发现每次的内容都追加在末尾,而不是覆盖。
5.4 用 bufio.Writer 带缓冲写入
频繁写入小数据时,每次都直接写磁盘效率很低。bufio.Writer 会先把数据攒在内存缓冲区里,攒够了一起写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| package main
import ( "bufio" "fmt" "os" )
func main() { file, err := os.Create("output.txt") if err != nil { fmt.Println("创建失败:", err) return } defer file.Close()
writer := bufio.NewWriter(file)
for i := 1; i <= 10000; i++ { fmt.Fprintf(writer, "第 %d 行\n", i) }
err = writer.Flush() if err != nil { fmt.Println("刷新缓冲区失败:", err) return }
fmt.Println("写入成功!") }
|
writer.Flush() 是最容易忘记的一步! 不调用它,数据可能还在内存缓冲区里,没有真正写入磁盘。如果你发现文件内容不完整,第一个要检查的就是有没有忘记 Flush。
带缓冲写入的工作原理
1 2 3 4 5 6
| 写入操作 缓冲区(默认4KB) 磁盘文件 ───────── ───────────── ──────── WriteString("abc") → [abc ] (还没写) WriteString("def") → [abcdef ] (还没写) ...攒到 4KB 了... → 自动刷入 → [abcdef...] Flush() → 手动刷入 → [剩余内容]
|
缓冲区满了会自动写入磁盘,但调用 Flush() 可以确保缓冲区里剩余的数据也写入。
6. 文件和目录的创建与删除
6.1 创建文件
1 2 3 4 5 6
| file, err := os.Create("newfile.txt") if err != nil { } defer file.Close()
|
6.2 创建目录
1 2 3 4 5
| err := os.Mkdir("mydir", 0755)
err := os.MkdirAll("path/to/deep/dir", 0755)
|
区别:
os.Mkdir:只能创建一层,父目录不存在就失败
os.MkdirAll:递归创建所有需要的父目录
1 2 3 4 5
| os.Mkdir("path/to/deep/dir", 0755)
os.MkdirAll("path/to/deep/dir", 0755)
|
6.3 删除文件和目录
1 2 3 4 5
| err := os.Remove("oldfile.txt")
err := os.RemoveAll("mydir")
|
os.RemoveAll 会递归删除整个目录树,不可恢复,务必小心!
6.4 重命名和移动文件
1 2
| err := os.Rename("old.txt", "new.txt")
|
6.5 综合示例:创建临时工作目录、写入文件、读取验证、清理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| package main
import ( "fmt" "os" "path/filepath" )
func main() { workDir := "workspace" err := os.MkdirAll(workDir, 0755) if err != nil { fmt.Println("创建目录失败:", err) return } fmt.Println("目录创建成功:", workDir)
filePath := filepath.Join(workDir, "data.txt") err = os.WriteFile(filePath, []byte("工作区数据\n"), 0644) if err != nil { fmt.Println("写入失败:", err) return } fmt.Println("文件写入成功:", filePath)
content, err := os.ReadFile(filePath) if err != nil { fmt.Println("读取失败:", err) return } fmt.Println("文件内容:", string(content))
err = os.RemoveAll(workDir) if err != nil { fmt.Println("清理失败:", err) return } fmt.Println("清理完成!") }
|
7. 路径操作
处理文件路径时,不要手动拼接字符串(Windows 用 \,Linux 用 /)。用 path/filepath 包来保证跨平台兼容。
7.1 filepath.Join:安全拼接路径
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package main
import ( "fmt" "path/filepath" )
func main() { path := filepath.Join("data", "users", "alice.json") fmt.Println(path) }
|
7.2 filepath.Dir 和 filepath.Base
1 2 3 4 5
| path := "/home/user/documents/report.txt"
dir := filepath.Dir(path) base := filepath.Base(path) ext := filepath.Ext(path)
|
7.3 filepath.Split
1 2 3
| dir, file := filepath.Split("/home/user/report.txt")
|
7.4 filepath.Abs:获取绝对路径
1 2 3 4 5 6 7 8
| abs, err := filepath.Abs("data/file.txt") if err != nil { fmt.Println(err) return } fmt.Println(abs)
|
7.5 filepath.Walk:遍历目录树
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| package main
import ( "fmt" "os" "path/filepath" )
func main() { err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error { if err != nil { return err }
if info.IsDir() { fmt.Printf("[目录] %s\n", path) } else { fmt.Printf("[文件] %s (%d 字节)\n", path, info.Size()) } return nil })
if err != nil { fmt.Println("遍历出错:", err) } }
|
filepath.Walk 的回调函数签名:
1
| func(path string, info os.FileInfo, err error) error
|
path:当前遍历到的路径
info:文件信息(大小、修改时间、是否目录等)
err:如果访问这个路径出错,这里会有值
- 返回值:返回非
nil 的 error 会终止遍历
实际例子:统计目录中所有 Go 文件的行数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| package main
import ( "bufio" "fmt" "os" "path/filepath" "strings" )
func main() { totalLines := 0 fileCount := 0
err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() { return nil } if !strings.HasSuffix(path, ".go") { return nil }
file, err := os.Open(path) if err != nil { return err } defer file.Close()
scanner := bufio.NewScanner(file) lines := 0 for scanner.Scan() { lines++ }
if err := scanner.Err(); err != nil { return err }
fmt.Printf("%s: %d 行\n", path, lines) totalLines += lines fileCount++ return nil })
if err != nil { fmt.Println("出错:", err) return }
fmt.Printf("共 %d 个 Go 文件,%d 行代码\n", fileCount, totalLines) }
|
7.6 常用路径函数速查
| 函数 |
用途 |
示例 |
filepath.Join(a, b) |
拼接路径 |
filepath.Join("a", "b") → a/b |
filepath.Dir(p) |
获取目录部分 |
filepath.Dir("a/b/c.txt") → a/b |
filepath.Base(p) |
获取文件名部分 |
filepath.Base("a/b/c.txt") → c.txt |
filepath.Ext(p) |
获取扩展名 |
filepath.Ext("c.txt") → .txt |
filepath.Split(p) |
拆分目录和文件名 |
返回 (dir, file) |
filepath.Abs(p) |
转绝对路径 |
相对路径 → 绝对路径 |
filepath.Rel(base, targ) |
计算相对路径 |
filepath.Rel("/a", "/a/b") → b |
filepath.Clean(p) |
清理路径 |
a//b/../c → a/c |
8. 文件信息:os.Stat
在操作文件之前,经常需要先检查文件是否存在、大小是多少等信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| package main
import ( "fmt" "os" )
func main() { info, err := os.Stat("data.txt") if err != nil { if os.IsNotExist(err) { fmt.Println("文件不存在") } else { fmt.Println("获取信息出错:", err) } return }
fmt.Println("文件名:", info.Name()) fmt.Println("大小:", info.Size(), "字节") fmt.Println("权限:", info.Mode()) fmt.Println("修改时间:", info.ModTime()) fmt.Println("是否目录:", info.IsDir()) }
|
输出示例:
1 2 3 4 5
| 文件名: data.txt 大小: 1024 字节 权限: -rw-r 修改时间: 2024-01-15 10:30:00 +0800 CST 是否目录: false
|
8.1 判断文件是否存在
1 2 3 4 5 6 7 8 9 10 11
| func fileExists(path string) bool { _, err := os.Stat(path) if err == nil { return true } if os.IsNotExist(err) { return false } return false }
|
注意:os.Stat 返回错误时,不要简单地判断 err != nil 就说文件不存在。用 os.IsNotExist(err) 精确判断。因为错误也可能是"权限不足"——文件是存在的,但你没权限查看。
8.2 用 os.Stat 做"存在则跳过,不存在则创建"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| func ensureFile(path string) error { _, err := os.Stat(path) if err == nil { fmt.Println("文件已存在,跳过创建") return nil } if !os.IsNotExist(err) { return err }
return os.WriteFile(path, []byte("默认内容\n"), 0644) }
|
9. 读取目录
9.1 os.ReadDir:读取目录下的条目
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| package main
import ( "fmt" "os" )
func main() { entries, err := os.ReadDir(".") if err != nil { fmt.Println("读取目录失败:", err) return }
for _, entry := range entries { if entry.IsDir() { fmt.Printf("[DIR] %s\n", entry.Name()) } else { info, _ := entry.Info() fmt.Printf("[FILE] %s (%d 字节)\n", entry.Name(), info.Size()) } } }
|
9.2 只列出特定扩展名的文件
1 2 3 4 5 6 7 8 9 10 11 12 13
| entries, err := os.ReadDir(".") if err != nil { return }
for _, entry := range entries { if entry.IsDir() { continue } if filepath.Ext(entry.Name()) == ".txt" { fmt.Println(entry.Name()) } }
|
10. 综合示例:简易日志系统
把前面学到的知识综合起来,做一个简单的日志写入和查询程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
| package main
import ( "bufio" "fmt" "os" "path/filepath" "strings" "time" )
const logFile = "app.log"
func WriteLog(level, message string) error { file, err := os.OpenFile(logFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) if err != nil { return fmt.Errorf("打开日志文件失败: %w", err) } defer file.Close()
timestamp := time.Now().Format("2006-01-02 15:04:05") logLine := fmt.Sprintf("[%s] [%s] %s\n", timestamp, level, message)
_, err = file.WriteString(logLine) return err }
func ReadLogs() ([]string, error) { if _, err := os.Stat(logFile); os.IsNotExist(err) { return nil, fmt.Errorf("日志文件不存在") }
file, err := os.Open(logFile) if err != nil { return nil, err } defer file.Close()
var logs []string scanner := bufio.NewScanner(file) for scanner.Scan() { logs = append(logs, scanner.Text()) } return logs, scanner.Err() }
func SearchLogs(keyword string) ([]string, error) { logs, err := ReadLogs() if err != nil { return nil, err }
var results []string for _, line := range logs { if strings.Contains(line, keyword) { results = append(results, line) } } return results, nil }
func main() { WriteLog("INFO", "程序启动") WriteLog("INFO", "用户 admin 登录成功") WriteLog("ERROR", "数据库连接超时") WriteLog("INFO", "用户 admin 下单成功") WriteLog("WARN", "内存使用率超过 80%") WriteLog("ERROR", "支付接口返回异常")
fmt.Println("=== 全部日志 ===") logs, err := ReadLogs() if err != nil { fmt.Println(err) return } for _, line := range logs { fmt.Println(line) }
fmt.Println("\n=== ERROR 日志 ===") errors, err := SearchLogs("ERROR") if err != nil { fmt.Println(err) return } for _, line := range errors { fmt.Println(line) }
fmt.Println("\n日志文件路径:") abs, _ := filepath.Abs(logFile) fmt.Println(abs) }
|
输出示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| === 全部日志 === [2024-01-15 10:00:00] [INFO] 程序启动 [2024-01-15 10:00:01] [INFO] 用户 admin 登录成功 [2024-01-15 10:00:02] [ERROR] 数据库连接超时 [2024-01-15 10:00:03] [INFO] 用户 admin 下单成功 [2024-01-15 10:00:04] [WARN] 内存使用率超过 80% [2024-01-15 10:00:05] [ERROR] 支付接口返回异常
=== ERROR 日志 === [2024-01-15 10:00:02] [ERROR] 数据库连接超时 [2024-01-15 10:00:05] [ERROR] 支付接口返回异常
日志文件路径: D:\your\project\app.log
|
11. 常见坑总结
11.1 忘记 defer file.Close()
后果:文件句柄泄漏。操作系统对每个进程能打开的文件数量有限制(Linux 默认 1024),泄漏多了就打不开新文件了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| func badRead(path string) { file, _ := os.Open(path) }
func goodRead(path string) error { file, err := os.Open(path) if err != nil { return err } defer file.Close() return nil }
|
11.2 忘记 writer.Flush()
后果:数据还在缓冲区里,没有写入磁盘。程序正常结束时可能会丢失数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| func badWrite(path string) { file, _ := os.Create(path) defer file.Close()
writer := bufio.NewWriter(file) for i := 0; i < 1000; i++ { fmt.Fprintln(writer, "line", i) } }
func goodWrite(path string) error { file, err := os.Create(path) if err != nil { return err } defer file.Close()
writer := bufio.NewWriter(file) for i := 0; i < 1000; i++ { fmt.Fprintln(writer, "line", i) }
return writer.Flush() }
|
11.3 手动拼接路径而不是用 filepath.Join
后果:在 Windows 上运行正常,部署到 Linux 上路径错误。
1 2 3 4 5
| path := "data" + "\\" + "file.txt"
path := filepath.Join("data", "file.txt")
|
11.4 用 os.Remove 删除非空目录
后果:报错 “directory not empty”。
1 2 3 4 5
| os.Remove("mydir/")
os.RemoveAll("mydir/")
|
11.5 忽略 os.Stat 的其他错误
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| _, err := os.Stat(path) if err != nil { return false }
_, err := os.Stat(path) if os.IsNotExist(err) { return false } if err != nil { return false } return true
|
11.6 Scanner 默认限制单行 64KB
bufio.Scanner 默认缓冲区是 64KB。如果文件中有超过 64KB 的单行,Scan() 会失败。
1 2 3 4 5 6
| scanner := bufio.NewScanner(file) scanner.Buffer(make([]byte, 1024*1024), 1024*1024) for scanner.Scan() { }
|
11.7 在循环中 defer 文件关闭
defer 只在函数返回时执行。如果在循环里 defer file.Close(),所有文件句柄会堆积到函数结束才关闭:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| func processAll(paths []string) error { for _, path := range paths { file, err := os.Open(path) if err != nil { return err } defer file.Close() } return nil }
func processAll(paths []string) error { for _, path := range paths { if err := func() error { file, err := os.Open(path) if err != nil { return err } defer file.Close() return nil }(); err != nil { return err } } return nil }
|
11.8 写入文件后立即读取
如果你用同一个文件句柄先写后读,必须先调用 file.Seek(0, 0) 把读写位置移回开头:
1 2 3 4 5 6 7 8 9 10 11
| file, _ := os.OpenFile("data.txt", os.O_RDWR|os.O_CREATE, 0644) defer file.Close()
file.WriteString("Hello")
file.Seek(0, 0)
reader := bufio.NewReader(file) line, _ := reader.ReadString('\n') fmt.Println(line)
|
12. io 包的核心接口(补充)
io 包定义了 Go 中最基础的 I/O 接口,理解这些能帮你更好地理解 os 和 bufio 的关系。
12.1 io.Reader 和 io.Writer
1 2 3 4 5 6 7
| type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
|
*os.File 实现了 io.Reader 和 io.Writer,这就是为什么 bufio.NewReader(file) 能工作——bufio.NewReader 接受的是 io.Reader,不关心具体是什么类型。
12.2 用 io.Copy 高效复制数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| package main
import ( "fmt" "io" "os" )
func main() { src, err := os.Open("source.txt") if err != nil { fmt.Println("打开源文件失败:", err) return } defer src.Close()
dst, err := os.Create("dest.txt") if err != nil { fmt.Println("创建目标文件失败:", err) return } defer dst.Close()
written, err := io.Copy(dst, src) if err != nil { fmt.Println("复制失败:", err) return }
fmt.Printf("复制完成,共 %d 字节\n", written) }
|
io.Copy 是复制文件的推荐方式,内部自动分块读写,比手动循环效率高。
13. 本课练习
练习 1:创建和读取文件
要求:
- 创建一个文件
students.txt,写入 5 个学生姓名(每行一个)
- 读取这个文件,打印每个学生姓名及其行号
练习 2:文件内容统计
要求:
- 读取一个文本文件
- 统计:总字符数、总行数、非空行数
- 提示:
scanner.Scan() + strings.TrimSpace
练习 3:简易 CSV 读取器
要求:
- 读取一个逗号分隔的文本文件(如
name,age,city)
- 解析每行,以整齐的格式打印(提示:
strings.Split)
- 示例文件
data.csv:1 2 3
| name,age,city Alice,25,Beijing Bob,30,Shanghai
|
练习 4:目录遍历与文件过滤
要求:
- 遍历指定目录及其子目录
- 找出所有
.txt 文件,打印路径和大小
- 统计所有
.txt 文件的总大小
练习 5:简易文件备份工具
要求:
- 接收一个文件路径作为输入
- 将文件复制一份,命名为
原文件名.bak
- 用
io.Copy 实现复制
14. 自测题
14.1 概念题
os.Open 和 os.OpenFile 有什么区别?
os.ReadFile 适合什么场景?大文件应该怎么做?
bufio.Scanner 和 bufio.Reader 各自适合什么场景?
writer.Flush() 为什么必须调用?不调用会怎样?
os.Mkdir 和 os.MkdirAll 有什么区别?
filepath.Join 相比手动拼接字符串有什么优势?
- 怎么判断一个文件是否存在?
io.Copy 的作用是什么?
os.Remove 能删除非空目录吗?
bufio.Scanner 默认能处理多大的单行?
14.2 代码阅读题
下面的代码有什么问题?
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| func countLines(path string) (int, error) { file, err := os.Open(path) if err != nil { return 0, err }
scanner := bufio.NewScanner(file) count := 0 for scanner.Scan() { count++ }
return count, nil }
|
点击查看答案
问题 1:缺少 defer file.Close(),文件句柄泄漏。
问题 2:没有检查 scanner.Err(),如果读取过程中出错,会当作正常读完处理。
修正后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| func countLines(path string) (int, error) { file, err := os.Open(path) if err != nil { return 0, err } defer file.Close()
scanner := bufio.NewScanner(file) count := 0 for scanner.Scan() { count++ }
if err := scanner.Err(); err != nil { return 0, err }
return count, nil }
|
15. 本课总结
这一课你学到了 Go 中文件操作的完整知识。
你现在应该已经理解:
| 操作 |
推荐方式 |
| 读取小文件 |
os.ReadFile |
| 读取大文件 |
bufio.Scanner(逐行)或 file.Read(分块) |
| 一次性写入 |
os.WriteFile |
| 分次写入 |
file.WriteString + bufio.Writer |
| 追加写入 |
os.OpenFile + O_APPEND |
| 判断文件是否存在 |
os.Stat + os.IsNotExist |
| 拼接路径 |
filepath.Join |
| 遍历目录 |
filepath.Walk / os.ReadDir |
| 复制文件 |
io.Copy |
最重要的三件事:
- 打开文件后
defer file.Close()
bufio.Writer 用完后 Flush()
- 路径操作用
filepath 包,不要手动拼接
16. 下一课预告
下一课我们学习时间处理。
会重点讲:
time.Now() 获取当前时间
- 时间格式化(Go 独特的格式化方式)
- 时间解析、计算、比较
- 定时器和
time.Sleep
学完下一课,你就能在程序中处理各种和时间相关的任务了。