border-home1

命令行任务管理工具(CLI To-Do List)

功能需求

 支持add(添加任务)、list(列出任务)、done(标记完成)。

设计思路

  1. 通过os.Args来读取并解析命令行参数
  2. 读取os.Args[1]然后通过swtich分发给add/list/done
  3. 核心函数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 读取快速读取整个文件内容。返回文件的全部 []byteerror
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 字节切片,它接收三个参数:

  1. tasks []Task 要序列化的 Go 数据。
  2. prefix string (第二个参数,空的 ""): 定义 JSON 每一行前缀。我们传入空字符串 "",表示行前不加任何字符。
  3. 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)字符串转整数 (简写)转换字符串 sint 类型。是最常用的简便形式。返回 (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) 占位符,它能包装底层错误,确保在应用层能追踪到失败的根源。
recent-work

命令行任务管理工具(CLI To-Do List)

功能需求

 支持add(添加任务)、list(列出任务)、done(标记完成)。

设计思路

  1. 通过os.Args来读取并解析命令行参数
  2. 读取os.Args[1]然后通过swtich分发给add/list/done
  3. 核心函数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操作的函数,以及错误处理的方式和序列化的知识,知识点总结如下

Read more →

external example

Teaser for your external link

Read more →
border-home1