章节

14.9 练习

本篇通过若干分级练习巩固文件读取、写入、复制与目录操作,并能独立完成基础文件处理程序。

练习

学完本章你应该掌握

  • 能根据文件大小和处理目标,选择 os.ReadFileFile.Readbufio.Scanner 三种不同的读取方式。
  • 能正确处理文件读写中的关键细节,例如 []bytestring 的转换、buffer[:n] 的使用、io.EOF 的含义、scanner.Err() 的检查。
  • 能使用 os.WriteFileos.OpenFile 完成覆盖写、追加写,以及常见打开方式和权限参数的组合。
  • 能使用 io.Copyos.MkdirAllos.ReadDiros.Removeos.RemoveAll 完成基础文件复制与目录管理。
  • 能把前面学过的函数、结构体、切片、map、错误返回、字符串处理等知识自然地用进文件处理程序里。
  • 能写出小型的文件工具,例如配置加载器、文件摘要生成器、成绩报告生成器和批量备份器。

简单

第 1 题:写入并读回一段课程说明

第 1 题 简单

编写一个 Go 程序,完成下面要求:

  • 使用 os.WriteFile 创建文件 lesson.txt
  • 向文件中写入文本:Go 文件操作很重要
  • 再使用 os.ReadFile 读回这份文件内容
  • 最后输出文件内容和字节长度

示例输出可以类似这样:

1
2
Go 文件操作很重要
bytes=24
查看参考答案
 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
package main

import (
	"fmt"
	"os"
)

func main() {
	path := "lesson.txt"

	err := os.WriteFile(path, []byte("Go 文件操作很重要"), 0644)
	if err != nil {
		fmt.Println("写入失败:", err)
		return
	}
	defer os.Remove(path)

	data, err := os.ReadFile(path)
	if err != nil {
		fmt.Println("读取失败:", err)
		return
	}

	fmt.Println(string(data))
	fmt.Printf("bytes=%d\n", len(data))
}

说明:os.WriteFileos.ReadFile 都适合处理小文件;读取结果是 []byte,要转成 string 才更适合按文本输出。

第 2 题:打印源码文件名和当前工作目录

第 2 题 简单

编写一个 Go 程序,完成下面要求:

  • 使用 runtime.Caller(0) 获取当前源码文件路径
  • 只输出文件名部分
  • 再使用 os.Getwd() 获取当前工作目录
  • 最后分别输出这两项内容

示例输出可以类似这样:

1
2
source=main.go
workdir=D:\study\demo
查看参考答案
 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"
	"path/filepath"
	"runtime"
)

func main() {
	_, file, _, ok := runtime.Caller(0)
	if !ok {
		fmt.Println("获取源码路径失败")
		return
	}

	workdir, err := os.Getwd()
	if err != nil {
		fmt.Println("获取工作目录失败:", err)
		return
	}

	fmt.Printf("source=%s\n", filepath.Base(file))
	fmt.Printf("workdir=%s\n", workdir)
}

说明:runtime.Caller 反映的是源码位置,os.Getwd() 反映的是程序运行时的当前工作目录,这两个概念不要混用。

第 3 题:按固定大小分片读取文件

第 3 题 简单

编写一个 Go 程序,完成下面要求:

  • 先创建文件 chunks.txt,内容为 abcdefghi
  • 再使用 os.Open 打开这个文件
  • 使用大小为 3 的缓冲区分片读取
  • 每读取到一段内容,就单独输出一行

期望输出:

1
2
3
abc
def
ghi
查看参考答案
 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
package main

import (
	"fmt"
	"io"
	"os"
)

func main() {
	path := "chunks.txt"
	err := os.WriteFile(path, []byte("abcdefghi"), 0644)
	if err != nil {
		fmt.Println("写入失败:", err)
		return
	}
	defer os.Remove(path)

	file, err := os.Open(path)
	if err != nil {
		fmt.Println("打开失败:", err)
		return
	}
	defer file.Close()

	buffer := make([]byte, 3)
	for {
		n, err := file.Read(buffer)
		if n > 0 {
			fmt.Println(string(buffer[:n]))
		}
		if err == io.EOF {
			break
		}
		if err != nil {
			fmt.Println("读取失败:", err)
			return
		}
	}
}

说明:分片读取时要始终使用 buffer[:n],因为最后一次读取的字节数不一定等于缓冲区长度。

第 4 题:逐行读取并输出行号

第 4 题 简单

编写一个 Go 程序,完成下面要求:

  • 创建文件 todo.txt
  • 写入下面三行内容:
1
2
3
复习变量
完成文件练习
整理笔记
  • 使用 bufio.Scanner 按行读取
  • 输出时为每一行加上行号

期望输出:

1
2
3
1: 复习变量
2: 完成文件练习
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
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	path := "todo.txt"
	content := "复习变量\n完成文件练习\n整理笔记\n"

	err := os.WriteFile(path, []byte(content), 0644)
	if err != nil {
		fmt.Println("写入失败:", err)
		return
	}
	defer os.Remove(path)

	file, err := os.Open(path)
	if err != nil {
		fmt.Println("打开失败:", err)
		return
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	lineNo := 1
	for scanner.Scan() {
		fmt.Printf("%d: %s\n", lineNo, scanner.Text())
		lineNo++
	}

	if err := scanner.Err(); err != nil {
		fmt.Println("扫描失败:", err)
	}
}

说明:逐行读取文本文件时,bufio.Scanner 往往比手动分片读取更直观,特别适合日志、清单和普通文本。

第 5 题:追加写入学习日志

第 5 题 简单

编写一个 Go 程序,完成下面要求:

  • 使用 os.OpenFile 以“创建 + 只写 + 追加”的方式打开 study.log
  • 依次写入两行内容:
1
2
第1次练习完成
第2次练习完成
  • 写入完成后,把整个文件再读出来并输出

期望输出:

1
2
第1次练习完成
第2次练习完成
查看参考答案
 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
package main

import (
	"fmt"
	"os"
)

func main() {
	path := "study.log"

	file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
	if err != nil {
		fmt.Println("打开失败:", err)
		return
	}
	defer os.Remove(path)
	defer file.Close()

	_, err = file.WriteString("第1次练习完成\n")
	if err != nil {
		fmt.Println("写入失败:", err)
		return
	}

	_, err = file.WriteString("第2次练习完成\n")
	if err != nil {
		fmt.Println("写入失败:", err)
		return
	}

	data, err := os.ReadFile(path)
	if err != nil {
		fmt.Println("读取失败:", err)
		return
	}

	fmt.Print(string(data))
}

说明:这里的关键是 os.O_APPEND,如果少了它,再次写入时就不再是“往后追加”的语义了。

一般

下面几题会自然用到前面学过的函数、切片、map、字符串处理和错误返回,但主考点仍然是文件的读取、写入、复制和目录操作。

第 6 题:修正一段分片读取程序

第 6 题 一般

下面程序想把 data.txt 的内容按块读出来,但现在有两个明显问题:

  • 最后一次读取可能会混入旧数据
  • 读到文件末尾时会直接报错退出

请改正代码,让它最终输出:

1
2
abcd
ef
 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() {
	path := "data.txt"
	os.WriteFile(path, []byte("abcdef"), 0644)
	defer os.Remove(path)

	file, _ := os.Open(path)
	defer file.Close()

	buffer := make([]byte, 4)
	for {
		n, err := file.Read(buffer)
		if err != nil {
			panic(err)
		}
		fmt.Println(string(buffer))
		if n == 0 {
			break
		}
	}
}
查看参考答案

修正后的代码如下:

 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
package main

import (
	"fmt"
	"io"
	"os"
)

func main() {
	path := "data.txt"
	err := os.WriteFile(path, []byte("abcdef"), 0644)
	if err != nil {
		fmt.Println("写入失败:", err)
		return
	}
	defer os.Remove(path)

	file, err := os.Open(path)
	if err != nil {
		fmt.Println("打开失败:", err)
		return
	}
	defer file.Close()

	buffer := make([]byte, 4)
	for {
		n, err := file.Read(buffer)
		if n > 0 {
			fmt.Println(string(buffer[:n]))
		}
		if err == io.EOF {
			break
		}
		if err != nil {
			fmt.Println("读取失败:", err)
			return
		}
	}
}

两个关键点分别是:

  • 输出时必须使用 buffer[:n],否则最后一次会把上轮残留数据一起带出来。
  • io.EOF 表示正常读到文件末尾,不应该直接当成普通错误 panic

第 7 题:封装一个 copyFile 函数

第 7 题 一般

编写一个 Go 程序,完成下面要求:

  • 编写函数 copyFile(srcPath, dstPath string) (int64, error)
  • 函数内部使用 os.Openos.Createio.Copy
  • 返回复制的字节数和错误信息
  • main 中先创建源文件 note.txt,内容为 copy me
  • 再把它复制到 backup.txt
  • 最后输出复制字节数,并读取 backup.txt 验证内容

期望输出:

1
2
copied=7
copy me
查看参考答案
 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
package main

import (
	"fmt"
	"io"
	"os"
)

func copyFile(srcPath, dstPath string) (int64, error) {
	src, err := os.Open(srcPath)
	if err != nil {
		return 0, err
	}
	defer src.Close()

	dst, err := os.Create(dstPath)
	if err != nil {
		return 0, err
	}
	defer dst.Close()

	return io.Copy(dst, src)
}

func main() {
	srcPath := "note.txt"
	dstPath := "backup.txt"

	err := os.WriteFile(srcPath, []byte("copy me"), 0644)
	if err != nil {
		fmt.Println("写入源文件失败:", err)
		return
	}
	defer os.Remove(srcPath)
	defer os.Remove(dstPath)

	n, err := copyFile(srcPath, dstPath)
	if err != nil {
		fmt.Println("复制失败:", err)
		return
	}

	data, err := os.ReadFile(dstPath)
	if err != nil {
		fmt.Println("读取目标文件失败:", err)
		return
	}

	fmt.Printf("copied=%d\n", n)
	fmt.Println(string(data))
}

说明:io.Copy 会返回实际复制的字节数,这很适合顺手做校验或打印复制结果。

第 8 题:创建、读取并删除练习目录

第 8 题 一般

编写一个 Go 程序,完成下面要求:

  • 创建目录结构:
1
2
3
practice_dir/
  notes/day1/
  cache/
  • 再创建文件 practice_dir/notes/day1/todo.txt
  • 读取 practice_dir 这一层的目录项,并输出每个条目的名称和是否为目录
  • 删除空目录 practice_dir/cache
  • 再次读取 practice_dir,输出剩余条目名称

提示:这道题会用到 os.MkdirAllos.ReadDiros.Remove

查看参考答案
 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
package main

import (
	"fmt"
	"os"
)

func printEntries(path string) error {
	entries, err := os.ReadDir(path)
	if err != nil {
		return err
	}

	for _, entry := range entries {
		fmt.Println(entry.Name(), entry.IsDir())
	}

	return nil
}

func main() {
	root := "practice_dir"

	err := os.MkdirAll(root+"/notes/day1", 0755)
	if err != nil {
		fmt.Println("创建 notes 目录失败:", err)
		return
	}

	err = os.MkdirAll(root+"/cache", 0755)
	if err != nil {
		fmt.Println("创建 cache 目录失败:", err)
		return
	}
	defer os.RemoveAll(root)

	err = os.WriteFile(root+"/notes/day1/todo.txt", []byte("完成目录练习"), 0644)
	if err != nil {
		fmt.Println("写入文件失败:", err)
		return
	}

	fmt.Println("第一次读取:")
	err = printEntries(root)
	if err != nil {
		fmt.Println("读取目录失败:", err)
		return
	}

	err = os.Remove(root + "/cache")
	if err != nil {
		fmt.Println("删除 cache 失败:", err)
		return
	}

	fmt.Println("第二次读取:")
	entries, err := os.ReadDir(root)
	if err != nil {
		fmt.Println("读取目录失败:", err)
		return
	}

	for _, entry := range entries {
		fmt.Println(entry.Name())
	}
}

说明:os.Remove 适合删除单个文件或空目录;如果目录里还有内容,通常要改用 os.RemoveAll

第 9 题:实现 readNonEmptyLines

第 9 题 一般

编写一个 Go 程序,完成下面要求:

  • 编写函数 readNonEmptyLines(path string) ([]string, error)
  • 这个函数逐行读取文件
  • 跳过空行和只包含空格的行
  • 返回所有“真正有内容”的行
  • main 中准备测试文件,内容如下:
1
2
3
4
5
Go

文件操作
  
练习
  • 调用函数后输出返回的切片和数量

期望输出可以类似这样:

1
2
[Go 文件操作 练习]
count=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
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
package main

import (
	"bufio"
	"fmt"
	"os"
	"strings"
)

func readNonEmptyLines(path string) ([]string, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	result := []string{}
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" {
			continue
		}
		result = append(result, line)
	}

	if err := scanner.Err(); err != nil {
		return nil, err
	}

	return result, nil
}

func main() {
	path := "lines.txt"
	content := "Go\n\n文件操作\n  \n练习\n"

	err := os.WriteFile(path, []byte(content), 0644)
	if err != nil {
		fmt.Println("写入失败:", err)
		return
	}
	defer os.Remove(path)

	lines, err := readNonEmptyLines(path)
	if err != nil {
		fmt.Println("读取失败:", err)
		return
	}

	fmt.Println(lines)
	fmt.Printf("count=%d\n", len(lines))
}

说明:strings.TrimSpace 会把一行首尾的空格和换行影响去掉,所以很适合做“空行过滤”。

第 10 题:把配置文件读成 map

第 10 题 一般

编写一个 Go 程序,完成下面要求:

  • 编写函数 loadConfig(path string) (map[string]string, error)
  • 配置文件每行格式为 key=value
  • 空行要跳过
  • # 开头的注释行也要跳过
  • main 中准备下面这份配置文件:
1
2
3
4
# app config
name=go-study
port=8080
mode=dev
  • 读取后输出:
1
2
3
name=go-study
port=8080
mode=dev
查看参考答案
 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
package main

import (
	"bufio"
	"fmt"
	"os"
	"strings"
)

func loadConfig(path string) (map[string]string, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	config := make(map[string]string)
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}

		parts := strings.SplitN(line, "=", 2)
		if len(parts) != 2 {
			return nil, fmt.Errorf("非法配置行:%s", line)
		}

		key := strings.TrimSpace(parts[0])
		value := strings.TrimSpace(parts[1])
		config[key] = value
	}

	if err := scanner.Err(); err != nil {
		return nil, err
	}

	return config, nil
}

func main() {
	path := "app.conf"
	content := "# app config\nname=go-study\nport=8080\nmode=dev\n"

	err := os.WriteFile(path, []byte(content), 0644)
	if err != nil {
		fmt.Println("写入失败:", err)
		return
	}
	defer os.Remove(path)

	config, err := loadConfig(path)
	if err != nil {
		fmt.Println("加载失败:", err)
		return
	}

	fmt.Printf("name=%s\n", config["name"])
	fmt.Printf("port=%s\n", config["port"])
	fmt.Printf("mode=%s\n", config["mode"])
}

说明:strings.SplitN(line, "=", 2) 表示最多只切两段,适合处理这种简单的 key=value 文本格式。

进阶

下面四题会把文件读写和前面学过的结构体、函数、map、错误处理、字符串转换,以及并发知识自然组合起来,更接近真实的小工具场景。

第 11 题:生成文件摘要结构体

第 11 题 进阶

编写一个 Go 程序,完成下面要求:

  • 定义结构体 FileSummary,字段包括: Name string Bytes int Lines int NonEmptyLines int
  • 编写函数 buildFileSummary(path string) (FileSummary, error)
  • 统计文件名、字节数、总行数、非空行数
  • main 中准备测试文件 summary.txt,内容如下:
1
2
3
4
go
file

read
  • 调用函数后输出摘要信息

期望输出可以类似这样:

1
2
3
4
name=summary.txt
bytes=14
lines=4
nonEmpty=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
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
package main

import (
	"bufio"
	"fmt"
	"os"
	"path/filepath"
	"strings"
)

type FileSummary struct {
	Name          string
	Bytes         int
	Lines         int
	NonEmptyLines int
}

func buildFileSummary(path string) (FileSummary, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return FileSummary{}, err
	}

	summary := FileSummary{
		Name:  filepath.Base(path),
		Bytes: len(data),
	}

	file, err := os.Open(path)
	if err != nil {
		return FileSummary{}, err
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		summary.Lines++
		if strings.TrimSpace(scanner.Text()) != "" {
			summary.NonEmptyLines++
		}
	}

	if err := scanner.Err(); err != nil {
		return FileSummary{}, err
	}

	return summary, nil
}

func main() {
	path := "summary.txt"
	content := "go\nfile\n\nread\n"

	err := os.WriteFile(path, []byte(content), 0644)
	if err != nil {
		fmt.Println("写入失败:", err)
		return
	}
	defer os.Remove(path)

	summary, err := buildFileSummary(path)
	if err != nil {
		fmt.Println("统计失败:", err)
		return
	}

	fmt.Printf("name=%s\n", summary.Name)
	fmt.Printf("bytes=%d\n", summary.Bytes)
	fmt.Printf("lines=%d\n", summary.Lines)
	fmt.Printf("nonEmpty=%d\n", summary.NonEmptyLines)
}

说明:这里把 os.ReadFilebufio.Scanner 组合起来了,前者适合快速拿字节数,后者更适合逐行统计。

第 12 题:从成绩文件生成统计报告

第 12 题 进阶

请完成一个稍微完整一点的小场景:

  • 定义结构体 Student,字段包括: Name string Score int
  • 准备成绩文件 scores.txt,内容如下:
1
2
3
小李 82
小王 59
小张 91
  • 编写函数 loadStudents(path string) ([]Student, error)
  • 使用 bufio.Scanner 按行读取,并把每一行解析成一个 Student
  • 编写函数 buildReport(students []Student) string
  • 统计学生人数、及格人数、平均分、最高分学生
  • 把报告写入 report.txt
  • 最后再读回 report.txt 并输出

期望输出可以类似这样:

1
2
3
4
count=3
passCount=2
avg=77.3
top=小张 91
查看参考答案
  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
102
103
104
105
106
107
108
109
110
111
112
113
114
package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

type Student struct {
	Name  string
	Score int
}

func loadStudents(path string) ([]Student, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	students := []Student{}
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" {
			continue
		}

		parts := strings.Fields(line)
		if len(parts) != 2 {
			return nil, fmt.Errorf("非法成绩行:%s", line)
		}

		score, err := strconv.Atoi(parts[1])
		if err != nil {
			return nil, fmt.Errorf("解析分数失败:%w", err)
		}

		students = append(students, Student{
			Name:  parts[0],
			Score: score,
		})
	}

	if err := scanner.Err(); err != nil {
		return nil, err
	}

	return students, nil
}

func buildReport(students []Student) string {
	total := 0
	passCount := 0
	top := students[0]

	for _, student := range students {
		total += student.Score
		if student.Score >= 60 {
			passCount++
		}
		if student.Score > top.Score {
			top = student
		}
	}

	avg := float64(total) / float64(len(students))

	return fmt.Sprintf(
		"count=%d\npassCount=%d\navg=%.1f\ntop=%s %d\n",
		len(students),
		passCount,
		avg,
		top.Name,
		top.Score,
	)
}

func main() {
	scorePath := "scores.txt"
	reportPath := "report.txt"
	content := "小李 82\n小王 59\n小张 91\n"

	err := os.WriteFile(scorePath, []byte(content), 0644)
	if err != nil {
		fmt.Println("写入成绩文件失败:", err)
		return
	}
	defer os.Remove(scorePath)
	defer os.Remove(reportPath)

	students, err := loadStudents(scorePath)
	if err != nil {
		fmt.Println("读取成绩失败:", err)
		return
	}

	report := buildReport(students)
	err = os.WriteFile(reportPath, []byte(report), 0644)
	if err != nil {
		fmt.Println("写入报告失败:", err)
		return
	}

	data, err := os.ReadFile(reportPath)
	if err != nil {
		fmt.Println("读取报告失败:", err)
		return
	}

	fmt.Print(string(data))
}

说明:

  • strings.Fields 可以把一行按空白切开,很适合这种“姓名 分数”的简单文本格式。
  • strconv.Atoi 用来把文本分数转换成整数,它不是本章重点,但在真实文件处理里很常见。

第 13 题:批量备份指定文件

第 13 题 进阶

编写一个 Go 程序,完成下面要求:

  • 编写函数 copyFile(srcPath, dstPath string) error
  • 再编写函数 backupFiles(srcFiles []string, backupDir string) error
  • backupFiles 要先创建备份目录
  • 再把传入的多个源文件逐个复制到备份目录中
  • main 中准备两个源文件: lesson1.txt lesson2.txt
  • 把它们备份到 backup_dir
  • 最后读取 backup_dir 并输出文件名

期望输出可以类似这样:

1
2
lesson1.txt
lesson2.txt
查看参考答案
 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
package main

import (
	"fmt"
	"io"
	"os"
)

func copyFile(srcPath, dstPath string) error {
	src, err := os.Open(srcPath)
	if err != nil {
		return err
	}
	defer src.Close()

	dst, err := os.Create(dstPath)
	if err != nil {
		return err
	}
	defer dst.Close()

	_, err = io.Copy(dst, src)
	return err
}

func backupFiles(srcFiles []string, backupDir string) error {
	err := os.MkdirAll(backupDir, 0755)
	if err != nil {
		return err
	}

	for _, srcPath := range srcFiles {
		dstPath := backupDir + "/" + srcPath
		if err := copyFile(srcPath, dstPath); err != nil {
			return err
		}
	}

	return nil
}

func main() {
	files := []string{"lesson1.txt", "lesson2.txt"}
	backupDir := "backup_dir"

	err := os.WriteFile("lesson1.txt", []byte("变量"), 0644)
	if err != nil {
		fmt.Println("写入 lesson1 失败:", err)
		return
	}

	err = os.WriteFile("lesson2.txt", []byte("文件"), 0644)
	if err != nil {
		fmt.Println("写入 lesson2 失败:", err)
		return
	}

	defer os.Remove("lesson1.txt")
	defer os.Remove("lesson2.txt")
	defer os.RemoveAll(backupDir)

	err = backupFiles(files, backupDir)
	if err != nil {
		fmt.Println("备份失败:", err)
		return
	}

	entries, err := os.ReadDir(backupDir)
	if err != nil {
		fmt.Println("读取备份目录失败:", err)
		return
	}

	for _, entry := range entries {
		fmt.Println(entry.Name())
	}
}

说明:这道题的重点是把“单文件复制”进一步封装成“多文件批量备份”,也就是把本章几个基础 API 组织成一个小工具。

第 14 题:实现一个并发批量备份器

第 14 题 进阶

请完成一个综合练习:

  • 编写函数 copyFile(srcPath, dstPath string) error
  • 准备三个源文件: a.txt b.txt c.txt
  • 创建备份目录 backup_async
  • 为每个源文件启动一个 goroutine 执行复制
  • 使用 sync.WaitGroup 等待所有复制任务结束
  • 使用 resultCh 收集每个任务的结果消息
  • 最后输出所有结果消息

结果消息可以类似这样:

1
2
3
a.txt -> ok
b.txt -> ok
c.txt -> ok

说明:结果输出顺序不固定。

查看参考答案
 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
package main

import (
	"fmt"
	"io"
	"os"
	"sync"
)

func copyFile(srcPath, dstPath string) error {
	src, err := os.Open(srcPath)
	if err != nil {
		return err
	}
	defer src.Close()

	dst, err := os.Create(dstPath)
	if err != nil {
		return err
	}
	defer dst.Close()

	_, err = io.Copy(dst, src)
	return err
}

func main() {
	files := []string{"a.txt", "b.txt", "c.txt"}
	backupDir := "backup_async"

	err := os.MkdirAll(backupDir, 0755)
	if err != nil {
		fmt.Println("创建备份目录失败:", err)
		return
	}
	defer os.RemoveAll(backupDir)

	for _, name := range files {
		err := os.WriteFile(name, []byte("content of "+name), 0644)
		if err != nil {
			fmt.Println("写入源文件失败:", err)
			return
		}
		defer os.Remove(name)
	}

	resultCh := make(chan string)
	var wg sync.WaitGroup

	for _, name := range files {
		wg.Add(1)
		go func(fileName string) {
			defer wg.Done()

			dstPath := backupDir + "/" + fileName
			err := copyFile(fileName, dstPath)
			if err != nil {
				resultCh <- fmt.Sprintf("%s -> failed: %v", fileName, err)
				return
			}

			resultCh <- fmt.Sprintf("%s -> ok", fileName)
		}(name)
	}

	go func() {
		wg.Wait()
		close(resultCh)
	}()

	for result := range resultCh {
		fmt.Println(result)
	}
}

说明:这道题虽然把 goroutine、WaitGroup 和 channel 带进来了,但核心任务依然是文件复制;并发只是让你顺手复习前面学过的程序组织方式。

本文禁止转载
使用 Hugo 构建
主题 StackJimmy 设计 由 Hobin 魔改
最近构建时间:2026-04-17 19:07:48 CST
载入天数...载入时分秒...
发表了 1 篇文章 · 发表了 152 篇笔记 · 总计 18 万 0 千字