Go 从 0 到精通 · 第 19 课:文件操作基础

学习定位:这是整套 Go 教程的第 19 课,也是阶段四(标准库与实战阶段)的第一课。
前置要求:已经完成第 18 课,掌握了包与模块管理。同时需要熟悉第 16 课的 error 处理和第 17 课的 defer
本课目标:掌握文件的创建、打开、读取、写入、删除,以及目录操作和路径处理,能编写简单但完整的文件处理程序。


1. 本课你要解决的核心问题

到目前为止,你写的程序都是从控制台输入、往控制台输出。但真实世界的程序几乎都要和文件打交道——读配置、写日志、处理数据、导出报告……

你需要搞明白以下问题:

  • 怎么打开和关闭文件
  • 怎么读取文件的全部内容或逐行读取
  • 怎么写入文件、追加内容
  • 怎么创建和删除文件、目录
  • 怎么处理文件路径
  • osiobufio 这几个包各自干什么

学完这一课,你就能让程序和磁盘上的文件交互了。


2. 先搞清楚几个核心包

在 Go 中做文件操作,主要用到三个包:

职责 一句话概括
os 操作系统接口 打开、创建、删除文件和目录
io I/O 原语 定义 Reader、Writer 接口
bufio 带缓冲的 I/O 逐行读取、高效写入

还有一小部分功能来自 path/filepath(路径操作)和 io/ioutil(在 Go 1.16 前常用的便捷函数,现在推荐用 osio 替代)。

先有个整体印象,下面逐个展开。


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() {
// os.Open 以只读方式打开文件
file, err := os.Open("hello.txt")
if err != nil {
fmt.Println("打开文件失败:", err)
return
}
// 重要:用完必须关闭!
defer file.Close()

fmt.Println("文件打开成功!")
}

关键点:

  • os.Open 只能读,不能写
  • 返回两个值:*os.Fileerror
  • 一定要调用 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 接受三个参数:

  1. 文件路径
  2. 打开模式(flag)
  3. 权限(仅在创建文件时生效)

常用打开模式

模式 含义
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() {
// 方式一:os.ReadFile(推荐,Go 1.16+)
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) // 每次读 1024 字节
for {
n, err := file.Read(buf)
if n > 0 {
// 处理 buf[:n] 中的数据
fmt.Printf("读取了 %d 字节\n", n)
}
if err != nil {
break // 读完了(io.EOF)或出错了
}
}
}

file.Read(buf) 的行为:

  • 返回两个值:实际读取的字节数 nerror
  • 读到文件末尾时,errio.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.ReaderScanner 更底层,功能更丰富:

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.Writefile.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()

// 方式一:Write 写入字节切片
_, err = file.Write([]byte("第一行内容\n"))
if err != nil {
fmt.Println("写入失败:", err)
return
}

// 方式二:WriteString 写入字符串(更方便)
_, 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() {
// O_APPEND 模式:追加到文件末尾
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)
}

// 非常重要!必须 Flush,否则数据可能还在缓冲区里没写入磁盘
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
// os.Create 创建或截断文件(等价于 OpenFile + O_CREATE + O_TRUNC)
file, err := os.Create("newfile.txt")
if err != nil {
// 处理错误
}
defer file.Close()

6.2 创建目录

1
2
3
4
5
// 创建单层目录
err := os.Mkdir("mydir", 0755)

// 递归创建多层目录(类似 mkdir -p)
err := os.MkdirAll("path/to/deep/dir", 0755)

区别:

  • os.Mkdir:只能创建一层,父目录不存在就失败
  • os.MkdirAll:递归创建所有需要的父目录
1
2
3
4
5
// 这个会失败,因为 path/to 不存在
os.Mkdir("path/to/deep/dir", 0755) // 错误!

// 这个能成功,自动创建 path、path/to、path/to/deep
os.MkdirAll("path/to/deep/dir", 0755) // 正确

6.3 删除文件和目录

1
2
3
4
5
// 删除单个文件
err := os.Remove("oldfile.txt")

// 删除目录及其所有内容(类似 rm -r)
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() {
// 1. 创建工作目录
workDir := "workspace"
err := os.MkdirAll(workDir, 0755)
if err != nil {
fmt.Println("创建目录失败:", err)
return
}
fmt.Println("目录创建成功:", workDir)

// 2. 在目录下创建文件
filePath := filepath.Join(workDir, "data.txt")
err = os.WriteFile(filePath, []byte("工作区数据\n"), 0644)
if err != nil {
fmt.Println("写入失败:", err)
return
}
fmt.Println("文件写入成功:", filePath)

// 3. 读回来验证
content, err := os.ReadFile(filePath)
if err != nil {
fmt.Println("读取失败:", err)
return
}
fmt.Println("文件内容:", string(content))

// 4. 清理
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)
// Windows: data\users\alice.json
// Linux: data/users/alice.json
}

7.2 filepath.Dirfilepath.Base

1
2
3
4
5
path := "/home/user/documents/report.txt"

dir := filepath.Dir(path) // /home/user/documents
base := filepath.Base(path) // report.txt
ext := filepath.Ext(path) // .txt

7.3 filepath.Split

1
2
3
dir, file := filepath.Split("/home/user/report.txt")
// dir = "/home/user/"
// file = "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)
// Windows: D:\your\project\data\file.txt
// Linux: /your/project/data/file.txt

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:如果访问这个路径出错,这里会有值
  • 返回值:返回非 nilerror 会终止遍历

实际例子:统计目录中所有 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
}
// 只处理 .go 文件
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/../ca/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--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"

// WriteLog 追加一条日志
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
}

// ReadLogs 读取所有日志
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()
}

// SearchLogs 搜索包含关键词的日志
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)
}

// 搜索 ERROR 日志
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)
}
// 忘了 Flush!数据可能不完整
}

// 正确做法
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
// 错误:Windows 下是 \,Linux 下是 /
path := "data" + "\\" + "file.txt" // Linux 上错了!

// 正确:跨平台兼容
path := filepath.Join("data", "file.txt")

11.4 用 os.Remove 删除非空目录

后果:报错 “directory not empty”。

1
2
3
4
5
// os.Remove 只能删除空目录或单个文件
os.Remove("mydir/") // 如果 mydir 不为空,失败!

// 用 os.RemoveAll 递归删除
os.RemoveAll("mydir/") // 删除 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) // 最大 1MB
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
// 错误:1000 个文件句柄要等到函数结束才关
func processAll(paths []string) error {
for _, path := range paths {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close() // 堆积!
// 处理 file...
}
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() // 每次循环结束都关闭
// 处理 file...
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")

// 不 Seek 的话,读的位置在 "Hello" 之后,读到的是空
file.Seek(0, 0) // 移到开头

reader := bufio.NewReader(file)
line, _ := reader.ReadString('\n')
fmt.Println(line) // "Hello"

12. io 包的核心接口(补充)

io 包定义了 Go 中最基础的 I/O 接口,理解这些能帮你更好地理解 osbufio 的关系。

12.1 io.Readerio.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.Readerio.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()

// io.Copy 内部用 32KB 缓冲区高效复制
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 概念题

  1. os.Openos.OpenFile 有什么区别?
  2. os.ReadFile 适合什么场景?大文件应该怎么做?
  3. bufio.Scannerbufio.Reader 各自适合什么场景?
  4. writer.Flush() 为什么必须调用?不调用会怎样?
  5. os.Mkdiros.MkdirAll 有什么区别?
  6. filepath.Join 相比手动拼接字符串有什么优势?
  7. 怎么判断一个文件是否存在?
  8. io.Copy 的作用是什么?
  9. os.Remove 能删除非空目录吗?
  10. 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

最重要的三件事:

  1. 打开文件后 defer file.Close()
  2. bufio.Writer 用完后 Flush()
  3. 路径操作用 filepath 包,不要手动拼接

16. 下一课预告

下一课我们学习时间处理

会重点讲:

  • time.Now() 获取当前时间
  • 时间格式化(Go 独特的格式化方式)
  • 时间解析、计算、比较
  • 定时器和 time.Sleep

学完下一课,你就能在程序中处理各种和时间相关的任务了。