命令行任务管理工具(CLI To-Do List)
功能需求
支持add(添加任务)、list(列出任务)、done(标记完成)。
设计思路
- 通过os.Args来读取并解析命令行参数
- 读取os.Args[1]然后通过swtich分发给add/list/done
- 核心函数loadTasks()和saveTasks()分别加载和保存任务(数据存储在json文件中)
数据模型
定义Task结构体,包含Description(任务内容),CreatedAt(创建时间),IsDone(是否完成)
具体流程
1.首先定义结构体
type Task struct {
Description string `json:"description"` // 任务描述
CreatedAt string `json:"createdAt"` // 创建时间
IsDone bool `json:"isDone"` // 是否完成
}
后面的json字段是为了添加映射,这里需要补充json序列化和反序列化的知识,
在 Go 语言中,进行 JSON 操作时,结构体的字段名需要满足 JSON 的规范,这涉及到 结构体标签(Struct Tags)。
- 问题: 你的结构体字段名使用了 Go 语言惯用的大驼峰命名(如
Desription)。但在 JSON 文件中,通用惯例是使用 小写或蛇形命名(如description)。 - 解决方案: 我们需要使用
json:"fieldName"标签来指导encoding/json包进行正确的映射。
2.处理命令行参数以及命令行分发代码
os.Args[0]:永远是程序的名称(例如:./todo)os.Args[1]:是第一个参数,即命令(例如:add)os.Args[2:]:是命令后面的所有参数
func main() {
//检查参数长度
if len(os.Args) < 2 {
fmt.Println("用法: todo <命令> [参数]")
fmt.Println("命令: add, list, done")
return
}
command := os.Args[1]
// 使用 switch 结构来分发命令
switch command {
case "add":
// ... 添加任务逻辑
case "list":
// ... 列出任务逻辑
case "done":
// ... 标记完成逻辑
default:
fmt.Printf("未知命令: %s\n", command)
}
}
3.实现核心函数loadTasks()和saveTasks()
这部分涉及很多io操作的函数,以及错误处理的方式和序列化的知识,知识点总结如下
常用文件 I/O (Input/Output) 操作函数
| 函数/常量 | 类型 | 功能 | 核心作用 |
|---|---|---|---|
os.ReadFile(name) | I/O 读取 | 快速读取整个文件内容。 | 返回文件的全部 []byte 和 error。 |
os.WriteFile(name, data, perm) | I/O 写入 | 将 []byte 数据写入文件,并设置文件权限。 | 实现数据的持久化(保存)。 |
os.Stat(name) | 文件信息 | 获取文件的元数据(大小、修改时间等)。 | 用于检查文件状态或是否存在。 |
os.Remove(name) | 文件操作 | 删除指定路径的文件。 | 用于清理或删除数据。 |
os.Exit(code) | 程序控制 | 立即终止程序运行。 | os.Exit(0) 为正常退出;os.Exit(1) 为错误退出。 |
os.IsNotExist(err) | 错误判断 | 检查传入的 err 是否是文件不存在的系统错误。 | 优雅地处理第一次运行或文件缺失的情况。 |
JSON 序列化与结构体标签
| 函数 | 类型 | 作用 | 适用场景 |
|---|---|---|---|
json.Unmarshal(data, &v) | 反序列化 | 将 []byte 格式的 JSON 数据解析并填充到 指针指向 的 Go 变量 v 中。 | 读取外部数据(如 API 响应、配置文件)。 |
json.Marshal(v) | 序列化 | 将 Go 变量转换为紧凑的、单行的 JSON []byte。 | 写入网络传输(API 响应),追求效率。 |
json.MarshalIndent(v, prefix, indent) | 格式化序列化 | 生成 带缩进 的 JSON 字符串(美化输出)。 | 写入本地文件、配置文件、调试日志。 |
json.NewDecoder / json.NewEncoder | 流式处理 | 用于处理大型 JSON 文件或网络流,避免一次性加载全部数据到内存。 | 高性能、大文件 I/O。 |
// --- 核心函数 1: 从文件中加载任务列表 ---
func loadTasks() ([]Task, error) {
// 1. 尝试读取文件内容
data, err := os.ReadFile(tasksFile)
// 2. 处理文件不存在的情况 (这是正常的,表示第一次运行)
if os.IsNotExist(err) {
return []Task{}, nil // 返回一个空的切片和 nil 错误
}
// 3. 处理其他 I/O 错误
if err != nil {
return nil, err
}
// 4. 反序列化 (Unmarshal) JSON 数据到 []Task
var tasks []Task
if err := json.Unmarshal(data, &tasks); err != nil {
return nil, fmt.Errorf("解析JSON失败: %w", err)
}
return tasks, nil
}
// --- 核心函数 2: 将任务列表保存到文件 ---
func saveTasks(tasks []Task) error {
// 1. 序列化 (Marshal) []Task 到 JSON 格式 (使用 MarshalIndent 格式化输出)
data, err := json.MarshalIndent(tasks, "", " ")
if err != nil {
return fmt.Errorf("JSON序列化失败: %w", err)
}
// 2. 写入文件 (权限 0644 表示用户可读写)
return os.WriteFile(tasksFile, data, 0644)
}
data, err := json.MarshalIndent(tasks, "", " ")
这行代码使用了 Go 语言的多重赋值:
data []byte: 变量data存储json.MarshalIndent函数的第一个返回值,即序列化后的 JSON 字节切片([]byte)。err error: 变量err存储第二个返回值,即操作过程中可能产生的错误。
这种模式(返回 结果, 错误)在 Go 标准库中是无处不在的,允许你在一行代码中接收操作结果并立即检查其是否失败。
json.MarshalIndent(tasks, "", " ")
json.MarshalIndent 函数用于将 Go 数据结构转换为格式化(带缩进)的 JSON 字节切片,它接收三个参数:
tasks []Task: 要序列化的 Go 数据。prefix string(第二个参数,空的""): 定义 JSON 每一行前缀。我们传入空字符串"",表示行前不加任何字符。indent string(第三个参数,带空格的" "): 定义 JSON 缩进字符串。我们传入两个空格" ",表示 JSON 的每个层级都使用两个空格进行缩进,让文件结构清晰易读。
这里展示第二三参数对json文件中读写的影响
二三参数都为空
[{"description":"购买牛奶","createdAt":"2025-12-08","isDone":false}]
第二个参数为#,第三个参数为俩空格
# [
# {
# "description": "购买牛奶",
# "createdAt": "2025-12-08",
# "isDone": false
# }
# ]
4.实现add/list/done函数
(1)add函数
通过新建新的task结构体存储新任务
case "add":
if len(os.Args) < 3 {
fmt.Println("❌ 错误: 请输入任务描述。用法: todo add <描述>")
os.Exit(1)
}
description := strings.Join(os.Args[2:], " ")
newTask := Task{
Description: description,
CreatedAt: time.Now().Format("2006-01-02 15:04:05"),//规定时间的格式
IsDone: false,
}
tasks, err := loadTasks()
if err != nil {
fmt.Fprintf(os.Stderr, "❌ 加载任务失败: %v\n", err)
os.Exit(1)
}
tasks = append(tasks, newTask) // Day 4: 切片 append
if err := saveTasks(tasks); err != nil {
fmt.Fprintf(os.Stderr, "❌ 保存任务失败: %v\n", err)
os.Exit(1)
}
fmt.Printf("✅ 任务添加成功: %s\n", newTask.Description)
(2)list函数
case "list":
tasks, err := loadTasks()
if err != nil {
fmt.Fprintf(os.Stderr, "❌ 加载任务失败: %v\n", err)
os.Exit(1)
}
fmt.Println("\n--- 待办事项列表 ---")
if len(tasks) == 0 {
fmt.Println("🎉 恭喜,任务列表为空!")
return
}
for i, t := range tasks {
status := "[ ]"
if t.IsDone {
status = "[X]"
}
// 打印格式: [状态] 序号. 描述 (创建时间)
fmt.Printf("%s %d. %s (创建于: %s)\n", status, i+1, t.Description, t.CreatedAt)
}
fmt.Println("----------------------\n")
(3)done函数
这里用到了strconv包,这个包是Go 中专门负责在 字符串 和 基本数据类型 之间进行转换的工具。
| 函数 | 作用 | 格式和返回值 |
|---|---|---|
Atoi(s string) | 字符串转整数 (简写) | 转换字符串 s 为 int 类型。是最常用的简便形式。返回 (int, error)。 |
ParseInt(s, base, bitSize) | 字符串转整数 (通用) | Atoi 的底层实现。允许你指定进制(base,如 10 进制、16 进制)和位数(bitSize,如 64 位)。 |
Itoa(i int) | 整数转字符串 (逆向) | 将 int 转换为对应的 string。用于程序需要将数字结果显示或写入文件时。 |
case "done":
if len(os.Args) < 3 {
fmt.Println("❌ 错误: 请指定要标记完成的任务序号。用法: todo done <序号>")
os.Exit(1)
}
indexStr := os.Args[2]
index, err := strconv.Atoi(indexStr)
//字符串转整数是很危险的操作,必须检查err
if err != nil || index <= 0 {
fmt.Println("❌ 错误: 无效的序号。请输入一个正整数。")
os.Exit(1)
}
tasks, err := loadTasks()
if err != nil {
fmt.Fprintf(os.Stderr, "❌ 加载任务失败: %v\n", err)
os.Exit(1)
}
// 检查索引是否越界
if index > len(tasks) {
fmt.Printf("❌ 错误: 序号 %d 超出列表范围 (%d个任务)。\n", index, len(tasks))
os.Exit(1)
}
// 标记任务完成 (切片索引是 index - 1)
tasks[index-1].IsDone = true
// 保存列表
if err := saveTasks(tasks); err != nil {
fmt.Fprintf(os.Stderr, "❌ 保存任务失败: %v\n", err)
os.Exit(1)
}
fmt.Printf("✅ 任务 %d: '%s' 已标记为完成。\n", index, tasks[index-1].Description)
完整代码
package main
import (
"encoding/json" // JSON序列化/反序列化
"fmt"
"os" // 文件I/O和命令行参数
"strconv" // 字符串转整数 (用于 'done' 命令)
"strings" // 字符串处理 (用于 'add' 命令)
"time" // 时间戳
)
// --- 核心配置与结构体 ---
const tasksFile = "tasks.json"
// Task 结构体 (Day 7 步骤一与二)
type Task struct {
Description string `json:"description"`
CreatedAt string `json:"createdAt"`
IsDone bool `json:"isDone"`
}
// --- 核心函数 1: 加载任务 ---
func loadTasks() ([]Task, error) {
data, err := os.ReadFile(tasksFile)
if os.IsNotExist(err) {
return []Task{}, nil // 文件不存在,返回空列表
}
if err != nil {
return nil, err
}
var tasks []Task
if err := json.Unmarshal(data, &tasks); err != nil {
return nil, fmt.Errorf("解析JSON失败: %w", err)
}
return tasks, nil
}
// --- 核心函数 2: 保存任务 ---
func saveTasks(tasks []Task) error {
data, err := json.MarshalIndent(tasks, "", " ")
if err != nil {
return fmt.Errorf("JSON序列化失败: %w", err)
}
// 写入文件,权限 0644
return os.WriteFile(tasksFile, data, 0644)
}
// --- 主逻辑:命令分发与实现 ---
func main() {
if len(os.Args) < 2 {
fmt.Println("用法: todo <命令> [参数] (add, list, done)")
os.Exit(0)
}
command := os.Args[1]
switch command {
// --- 1. ADD 命令实现 (Day 7 步骤四) ---
case "add":
if len(os.Args) < 3 {
fmt.Println("❌ 错误: 请输入任务描述。用法: todo add <描述>")
os.Exit(1)
}
description := strings.Join(os.Args[2:], " ")
newTask := Task{
Description: description,
CreatedAt: time.Now().Format("2006-01-02 15:04:05"),
IsDone: false,
}
tasks, err := loadTasks()
if err != nil {
fmt.Fprintf(os.Stderr, "❌ 加载任务失败: %v\n", err)
os.Exit(1)
}
tasks = append(tasks, newTask) // Day 4: 切片 append
if err := saveTasks(tasks); err != nil {
fmt.Fprintf(os.Stderr, "❌ 保存任务失败: %v\n", err)
os.Exit(1)
}
fmt.Printf("✅ 任务添加成功: %s\n", newTask.Description)
// --- 2. LIST 命令实现 (Day 7 步骤七) ---
case "list":
tasks, err := loadTasks()
if err != nil {
fmt.Fprintf(os.Stderr, "❌ 加载任务失败: %v\n", err)
os.Exit(1)
}
fmt.Println("\n--- 待办事项列表 ---")
if len(tasks) == 0 {
fmt.Println("🎉 恭喜,任务列表为空!")
return
}
for i, t := range tasks {
status := "[ ]"
if t.IsDone {
status = "[X]"
}
// 打印格式: [状态] 序号. 描述 (创建时间)
fmt.Printf("%s %d. %s (创建于: %s)\n", status, i+1, t.Description, t.CreatedAt)
}
fmt.Println("----------------------\n")
// --- 3. DONE 命令实现 ---
case "done":
if len(os.Args) < 3 {
fmt.Println("❌ 错误: 请指定要标记完成的任务序号。用法: todo done <序号>")
os.Exit(1)
}
// 字符串转整数 (复习)
indexStr := os.Args[2]
index, err := strconv.Atoi(indexStr)
if err != nil || index <= 0 {
fmt.Println("❌ 错误: 无效的序号。请输入一个正整数。")
os.Exit(1)
}
tasks, err := loadTasks()
if err != nil {
fmt.Fprintf(os.Stderr, "❌ 加载任务失败: %v\n", err)
os.Exit(1)
}
// 检查索引是否越界
if index > len(tasks) {
fmt.Printf("❌ 错误: 序号 %d 超出列表范围 (%d个任务)。\n", index, len(tasks))
os.Exit(1)
}
// 标记任务完成 (切片索引是 index - 1)
tasks[index-1].IsDone = true
// 保存列表
if err := saveTasks(tasks); err != nil {
fmt.Fprintf(os.Stderr, "❌ 保存任务失败: %v\n", err)
os.Exit(1)
}
fmt.Printf("✅ 任务 %d: '%s' 已标记为完成。\n", index, tasks[index-1].Description)
default:
fmt.Printf("❌ 未知命令: %s\n", command)
os.Exit(1)
}
}
使用方式
# 1. 添加任务
$ go run main.go add 完成 Day 7 总结
# 2. 查看列表
$ go run main.go list
# 3. 标记第一个任务完成
$ go run main.go done 1
总结
本次深度分析的核心在于理解 Go 语言如何实现数据在内存和文件之间的往返
1. 文件操作的稳定性 :
- 职责分离: 明确了
os.ReadFile用于读,os.WriteFile用于写。 - 健壮性: 学会了使用
os.IsNotExist(err)优雅地处理文件缺失(第一次运行)的情况,避免程序崩溃。
2. 数据转换的桥梁:
- 序列化: 使用
json.MarshalIndent将 Go 内存中的[]Task结构体转换为文件所需的 JSON 字节切片 ([]byte)。 - 反序列化: 使用
json.Unmarshal将文件内容转换回 Go 的[]Task(必须传递指针进行修改)。 - 可读性: 理解了
json.MarshalIndent中的参数(""," ")用于美化输出,方便调试。
3. 错误处理链条:
- 理解了 Go 函数中
(result, error)的标准返回值模式。 - 掌握了
fmt.Errorf("... %w", err)占位符,它能包装底层错误,确保在应用层能追踪到失败的根源。


external example