先阅读 main.go 中 run 的流程,再分别理解 appendLine 和 readLines,最后运行 main_test.go。本课共享根目录的 go.mod,仅用标准库,不安装第三方依赖。
本例每次在系统临时目录下新建独立的 go-lesson17-... 文件夹,只操作自己创建的 notes.txt,不覆盖仓库或用户已有文件。运行结束保留目录,方便你按终端显示的路径查看。多次运行会产生多个目录,学习完可以人工确认路径后删除;示例不自动递归删除。
path := filepath.Join(directory, "notes.txt")
filepath 处理当前操作系统的文件路径:
相对路径是相对于进程当前工作目录,不一定是源码目录或 exe 所在目录。比如从仓库根目录运行时,notes.txt 默认指向根目录下的文件。需要确认时可调用 os.Getwd()。
本课使用 os.MkdirTemp 返回的目录避免上述歧义。Join 只是拼路径,不创建目录,也不是防止不可信路径逃逸的安全校验;不要把用户任意输入直接作为敏感文件路径。
err := os.WriteFile(path, []byte(content), 0600)
data, err := os.ReadFile(path)
text := string(data)
[]byte(content):将字符串转换为字节切片,不是数组声明。string(data):将字节转换为字符串显示;不会自动检测或转换 GBK 等编码,本课文本使用 UTF-8。整体读取适合较小文件,大文件应考虑流式处理。WriteFile 不会自动创建缺失的父目录;需要时先用 os.MkdirAll 创建目录。
0600 是以 0 开头的八进制权限写法,在 Unix 中表示仅所有者可读写;它不是十进制 600。权限会受系统机制影响,Windows 上不能用它代替完整 ACL 配置。文件已存在时,此参数也不会自动修改已有权限。
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600)
这里 file 是 *os.File,包含对打开文件的操作方法。
| 标志 | 含义 |
|---|---|
| O_WRONLY | 只写 |
| O_CREATE | 文件不存在则创建 |
| O_APPEND | 写到末尾,不清空原内容 |
| 是按位或,用来组合标志,不是布尔运算的 ||。
WriteString 返回写入的字节数和 error。本课用 _ 忽略字节数,但保留并检查错误。写入后必须关闭;即使写入失败也要尝试 Close。
appendLine 显式保存 writeErr 和 closeErr,优先报告写入错误,没有写入错误时再返回关闭错误。这是简单的错误优先策略,不是同时保留两个错误;后续可学习 errors.Join。
关闭并不等于持久化到物理磁盘的全部保证,涉及重要数据时还要考虑 Sync、原子替换等问题,本课不展开。
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
Open 默认以只读方式打开,成功后才登记关闭操作。不能在检查打开错误之前就 defer 一个可能无效的对象。
这里是只读示例,简化为忽略关闭错误;写文件时不能照搬成“一律不检查关闭错误”。defer 在 readLines 返回前执行,不会等整个程序结束。
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
if err := scanner.Err(); err != nil {
return nil, err
}
os.Stat 获取文件信息,Name 是名称,IsDir 判断是否目录,Size 是字节数,不是中文字数。
不存在、无权限、路径指向目录等情况都可能返回错误,不要仅凭错误文字判断类别。本课先传播错误,后续可用 errors.Is 配合 os.ErrNotExist 等检查类别。
main 调用 run,由 run 返回错误。这样辅助函数有机会先执行 defer,再由 main 输出到 os.Stderr 并用 os.Exit(1) 表示失败。不要在需要依赖当前函数 defer 的位置直接 os.Exit,因为它不会执行 defer。
在仓库根目录执行:
New-Item -ItemType Directory -Force bin/17_files | Out-Null
go build -o ./bin/17_files/files.exe ./lessons/17_files
.\bin\17_files\files.exe
修改源码后需要重新编译。快速编译并运行:
go run ./lessons/17_files
运行后打开终端打印的“示例目录”,查看 notes.txt。目录路径每次不同,其余主要输出如下:
文件名:notes.txt,扩展名:.txt
=== 整体读取 ===
第17课:文件读写
复习 defer
=== 追加后逐行读取 ===
第 1 行:第17课:文件读写
第 2 行:复习 defer
第 3 行:学习逐行读取
文件名:notes.txt,是否目录:false,行数:3
在仓库根目录执行:
go test -count=1 -v ./lessons/17_files
测试使用 t.TempDir 创建隔离目录,测试结束后由框架清理,避免固定文件名冲突或污染项目。它与普通程序刻意保留的演示目录是两回事。
覆盖场景:不存在文件的追加创建、两次追加、中文文本、空文件和缺失文件错误。
不要把覆盖或删除练习指向自己的重要资料;任何删除前先核对完整绝对路径。若另行手敲,放到本课的 practice 子目录,用 go run ./lessons/17_files/practice 运行,不要在同一个包中重复声明 main 或同名函数。
下一课学习 JSON 编码与解码,把结构体保存为易交换的文本数据。