|
|
@@ -0,0 +1,160 @@
|
|
|
+# 第 17 课:文件读写与路径操作
|
|
|
+
|
|
|
+## 学习顺序
|
|
|
+
|
|
|
+先阅读 [main.go](./main.go) 中 run 的流程,再分别理解 appendLine 和 readLines,最后运行 [main_test.go](./main_test.go)。本课共享根目录的 go.mod,仅用标准库,不安装第三方依赖。
|
|
|
+
|
|
|
+本例每次在系统临时目录下新建独立的 `go-lesson17-...` 文件夹,只操作自己创建的 notes.txt,不覆盖仓库或用户已有文件。**运行结束保留目录**,方便你按终端显示的路径查看。多次运行会产生多个目录,学习完可以人工确认路径后删除;示例不自动递归删除。
|
|
|
+
|
|
|
+## 1. 路径和当前工作目录
|
|
|
+
|
|
|
+```go
|
|
|
+path := filepath.Join(directory, "notes.txt")
|
|
|
+```
|
|
|
+
|
|
|
+filepath 处理当前操作系统的文件路径:
|
|
|
+
|
|
|
+- Join:拼接路径,使用系统适合的分隔符。
|
|
|
+- Base:取最后一段文件名。
|
|
|
+- Ext:取扩展名,例如 .txt。
|
|
|
+- Dir:取所在目录。
|
|
|
+- Abs:将相对路径转成绝对路径,返回路径和 error。
|
|
|
+
|
|
|
+相对路径是相对于**进程当前工作目录**,不一定是源码目录或 exe 所在目录。比如从仓库根目录运行时,`notes.txt` 默认指向根目录下的文件。需要确认时可调用 `os.Getwd()`。
|
|
|
+
|
|
|
+本课使用 os.MkdirTemp 返回的目录避免上述歧义。Join 只是拼路径,不创建目录,也不是防止不可信路径逃逸的安全校验;不要把用户任意输入直接作为敏感文件路径。
|
|
|
+
|
|
|
+## 2. 整体写入与读取
|
|
|
+
|
|
|
+```go
|
|
|
+err := os.WriteFile(path, []byte(content), 0600)
|
|
|
+data, err := os.ReadFile(path)
|
|
|
+text := string(data)
|
|
|
+```
|
|
|
+
|
|
|
+- `[]byte(content)`:将字符串转换为字节切片,不是数组声明。
|
|
|
+- WriteFile:打开或创建文件、写入内容并关闭,调用方不需要再 Close。
|
|
|
+- **文件已存在时,WriteFile 会截断并覆盖原内容**,它不是追加。
|
|
|
+- ReadFile:一次读取整个文件并关闭,返回字节切片和错误。
|
|
|
+- `string(data)`:将字节转换为字符串显示;不会自动检测或转换 GBK 等编码,本课文本使用 UTF-8。
|
|
|
+
|
|
|
+整体读取适合较小文件,大文件应考虑流式处理。WriteFile 不会自动创建缺失的父目录;需要时先用 os.MkdirAll 创建目录。
|
|
|
+
|
|
|
+`0600` 是以 0 开头的八进制权限写法,在 Unix 中表示仅所有者可读写;它不是十进制 600。权限会受系统机制影响,Windows 上不能用它代替完整 ACL 配置。文件已存在时,此参数也不会自动修改已有权限。
|
|
|
+
|
|
|
+## 3. 打开文件并追加
|
|
|
+
|
|
|
+```go
|
|
|
+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、原子替换等问题,本课不展开。
|
|
|
+
|
|
|
+## 4. 逐行读取与 defer
|
|
|
+
|
|
|
+```go
|
|
|
+file, err := os.Open(path)
|
|
|
+if err != nil {
|
|
|
+ return nil, err
|
|
|
+}
|
|
|
+defer file.Close()
|
|
|
+```
|
|
|
+
|
|
|
+Open 默认以只读方式打开,成功后才登记关闭操作。不能在检查打开错误之前就 defer 一个可能无效的对象。
|
|
|
+
|
|
|
+这里是只读示例,简化为忽略关闭错误;写文件时不能照搬成“一律不检查关闭错误”。defer 在 readLines 返回前执行,不会等整个程序结束。
|
|
|
+
|
|
|
+```go
|
|
|
+scanner := bufio.NewScanner(file)
|
|
|
+for scanner.Scan() {
|
|
|
+ lines = append(lines, scanner.Text())
|
|
|
+}
|
|
|
+if err := scanner.Err(); err != nil {
|
|
|
+ return nil, err
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- Scan:读取下一个 token,本例默认按行分割。
|
|
|
+- Text:取得本行文本,不包含结尾换行符。
|
|
|
+- Scan 返回 false:可能是正常读完,也可能发生错误,所以循环后要检查 Err。
|
|
|
+- 默认 Scanner 有 token 大小限制,约 64 KiB,极长行可能报错。可以在开始扫描前用 Buffer 调整,或用 bufio.Reader 处理;逐行读取不等于没有长度限制。
|
|
|
+- 本函数最后仍把所有行存到切片,因此总内存随文件内容增长,不是恒定内存的大文件处理方案。
|
|
|
+
|
|
|
+## 5. 文件信息与错误
|
|
|
+
|
|
|
+`os.Stat` 获取文件信息,Name 是名称,IsDir 判断是否目录,Size 是字节数,不是中文字数。
|
|
|
+
|
|
|
+不存在、无权限、路径指向目录等情况都可能返回错误,不要仅凭错误文字判断类别。本课先传播错误,后续可用 errors.Is 配合 os.ErrNotExist 等检查类别。
|
|
|
+
|
|
|
+main 调用 run,由 run 返回错误。这样辅助函数有机会先执行 defer,再由 main 输出到 os.Stderr 并用 os.Exit(1) 表示失败。不要在需要依赖当前函数 defer 的位置直接 os.Exit,因为它不会执行 defer。
|
|
|
+
|
|
|
+## 编译并运行
|
|
|
+
|
|
|
+在**仓库根目录**执行:
|
|
|
+
|
|
|
+```powershell
|
|
|
+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
|
|
|
+```
|
|
|
+
|
|
|
+修改源码后需要重新编译。快速编译并运行:
|
|
|
+
|
|
|
+```powershell
|
|
|
+go run ./lessons/17_files
|
|
|
+```
|
|
|
+
|
|
|
+运行后打开终端打印的“示例目录”,查看 notes.txt。目录路径每次不同,其余主要输出如下:
|
|
|
+
|
|
|
+```text
|
|
|
+文件名:notes.txt,扩展名:.txt
|
|
|
+=== 整体读取 ===
|
|
|
+第17课:文件读写
|
|
|
+复习 defer
|
|
|
+=== 追加后逐行读取 ===
|
|
|
+第 1 行:第17课:文件读写
|
|
|
+第 2 行:复习 defer
|
|
|
+第 3 行:学习逐行读取
|
|
|
+文件名:notes.txt,是否目录:false,行数:3
|
|
|
+```
|
|
|
+
|
|
|
+## 测试
|
|
|
+
|
|
|
+在仓库根目录执行:
|
|
|
+
|
|
|
+```powershell
|
|
|
+go test -count=1 -v ./lessons/17_files
|
|
|
+```
|
|
|
+
|
|
|
+测试使用 t.TempDir 创建隔离目录,测试结束后由框架清理,避免固定文件名冲突或污染项目。它与普通程序刻意保留的演示目录是两回事。
|
|
|
+
|
|
|
+覆盖场景:不存在文件的追加创建、两次追加、中文文本、空文件和缺失文件错误。
|
|
|
+
|
|
|
+## 动手练习
|
|
|
+
|
|
|
+- [ ] 修改初始文本,查看整体读取和逐行读取的区别。
|
|
|
+- [ ] 多调用一次 appendLine,核对最终行数。
|
|
|
+- [ ] 输出 info.Size,比较字节数与中文字数。
|
|
|
+- [ ] 调用 os.Getwd,确认当前工作目录。
|
|
|
+- [ ] 只在本例自己的临时文件上再次调用 WriteFile,验证旧内容被覆盖。
|
|
|
+- [ ] 为“一行内容没有结尾换行符”的文件增加测试,观察 Scanner 是否能读取。
|
|
|
+
|
|
|
+不要把覆盖或删除练习指向自己的重要资料;任何删除前先核对完整绝对路径。若另行手敲,放到本课的 practice 子目录,用 `go run ./lessons/17_files/practice` 运行,不要在同一个包中重复声明 main 或同名函数。
|
|
|
+
|
|
|
+下一课学习 JSON 编码与解码,把结构体保存为易交换的文本数据。
|
|
|
+
|
|
|
+[上一课:接口](../16_interfaces/README.md) · [学习总览](../../README.md)
|