每日一Go-15、Go语言基础回顾与项目实战之任务清单

28 阅读2分钟

综合前面14天学习的内容,今天我们来做一个小项目--命令行任务清单管理器,功能包括:对任务清单的增删查改操作以及任务清单的数据持久化(保存为json文件)。

学习目的:综合运用前面14天学习的基础内容,包括:变量与常量、函数、结构体、接口、控制流程、切片与映射、错误处理等。

1、项目结构设计

$ tree                                                                                                                                        
.                                                                                                                                             
|-- data.json  // 存储任务数据                                                                                                                               
|-- go.mod     // go工程文件                                                                                                                             
|-- main.go    // 程序入口                                                                                                                               
|-- main_test.go // 测试用例                                                                                                                             
|-- models       // 模型包                                                                                                                             
|   `-- model.go // 任务模型                                                                                                                             
`-- services     // 服务包                                                                                                                             
    |-- interface.go // 接口定义                                                                                                                         
    `-- service.go   // 任务服务

2、开始创建项目

// 创建文件夹todolist
$ mkdir todolist 
// 进入文件夹todolist
$ cd todolist
// 创建go.mod文件
$ go mod init todolist                                                                                                                        
go: creating new go.mod: module todolist 
// 用vscode打开项目
$ code . 

3、功能分解与代码讲解3.1 定义models


// models/model.go
package models
//任务模型
type Todo struct {
    // 任务id
    ID int `json:"id"`
    // 任务内容
    Content string `json:"content"`
    // 完成状态
    Done bool `json:"done"`
}

// 给类型定义别名
type ctxKey string
// 定义常量 IdKey
const IdKey ctxKey = "id"

3.2 添加任务服务

// services/interface.go
package services
import (
    "context"
    "todolist/models"
)
type TodoInterface interface {
    LoadTasks(c context.Context, filename string) ([]models.Todo, error)
    SaveTasks(c context.Context, filename string, todos []models.Todo) error
    AddTask(c context.Context, filename string, content string, ch chan string) (int, error)
    CompleteTask(c context.Context, filename string, id int) error
    DeleteTask(c context.Context, filename string) error
}
// services/service.go
package services
import (
    "context"
    "encoding/json"
    "os"
    "time"
    "todolist/models"
    "github.com/spf13/cast"
)
type TodoService struct {
}
func NewTodoService() *TodoService {
    return &TodoService{}
}
// 读任务列表
func (s *TodoService) LoadTasks(c context.Context, filename string) (list []models.Todo, err error) {
    data, err := os.ReadFile(filename)
    if err != nil {
        if os.IsNotExist(err) {
            return []models.Todo{}, nil
        }
        return
    }
    err = json.Unmarshal(data, &list)
    return
}
// 保存任务
func (s *TodoService) SaveTasks(c context.Context, filename string, todos []models.Todo) (err error) {
    data, err := json.MarshalIndent(todos, "", " ")
    if err != nil {
        return
    }
    err = os.WriteFile(filename, data, 0644)
    return
}
// 添加任务
func (s *TodoService) AddTask(c context.Context, filename string, content string, ch chan string) (id int, err error) {
    data, err := s.LoadTasks(c, filename)
    if err != nil {
        ch <- "no data"
        return
    }
    id = int(time.Now().Unix())
    todo := models.Todo{
        ID:      id,
        Content: content,
        Done:    false,
    }
    data = append(data, todo)
    err = s.SaveTasks(c, filename, data)
    if err == nil {
        ch <- "success"
    }
    return
}
// 完成任务
func (s *TodoService) CompleteTask(c context.Context, filename string, id int) (err error) {
    data, err := s.LoadTasks(c, filename)
    if err != nil {
        return
    }
    for k := range data {
        if data[k].ID == id {
            data[k].Done = true
            break
        }
    }
    err = s.SaveTasks(c, filename, data)
    return
}
// 删除任务
func (s *TodoService) DeleteTask(c context.Context, filename string) (err error) {
    data, err := s.LoadTasks(c, filename)
    if err != nil {
        return
    }
    id := c.Value(models.IdKey)
    for k, itm := range data {
        if itm.ID == cast.ToInt(id) {
            data = append(data[:k], data[k+1:]...)
            break
        }
    }
    return s.SaveTasks(c, filename, data)
}

3.3 测试用例main_test.go


package main
import (
    "context"
    "fmt"
    "log"
    "testing"
    "time"
    "todolist/models"
    "todolist/services"
)
func TestAdd(t *testing.T) {
    service := services.NewTodoService()
    ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
    defer cancel()
    ch := make(chan string)
    go func() {
        if id, err := service.AddTask(ctx, dataFile, "学习人工智能", ch); err != nil {
            fmt.Println("添加失败:", err)
        } else {
            fmt.Println("任务添加成功,id:", id)
        }
    }()
    select {
    case <-ctx.Done():
        fmt.Println("超时:", ctx.Err())
    case <-ch:
        fmt.Println("写入成功")
    }
}
func TestDel(t *testing.T) {
    service := services.NewTodoService()
    id := 1762935143
    ctx := context.WithValue(context.Background(), models.IdKey, id)
    err := service.DeleteTask(ctx, dataFile)
    if err != nil {
        log.Println(err)
    }
    log.Println(`delete success`)
}
func TestInterface(t *testing.T) {
    var todoInterface services.TodoInterface
    todoInterface = services.NewTodoService()
    if _, ok := todoInterface.(services.TodoInterface); ok {
        log.Println(`matched`)
    } else {
        log.Println(`not matched`)
    }
}

3.4 main.go:命令解析与入口


package main
import (
    "context"
    "fmt"
    "os"
    "strconv"
    "time"
    "todolist/models"
    "todolist/services"
)
const dataFile = `data.json`
func main() {
    if len(os.Args) < 2 {
        fmt.Println("用法示例:")
        fmt.Println(` todo add <任务内容>`)
        fmt.Println(` todo list`)
        fmt.Println(` todo done <任务编号>`)
        fmt.Println(` todo del <任务编号>`)
        return
    }
    service := services.NewTodoService()
    switch os.Args[1] {
    case `add`:
        if len(os.Args) < 3 {
            fmt.Println("请提供任务内容")
            return
        }
        content := os.Args[2]
        ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
        defer cancel()
        ch := make(chan string)
        go func() {
            // time.Sleep(10 * time.Second) //模拟超时
            if id, err := service.AddTask(ctx, dataFile, content, ch); err != nil {
                fmt.Println("添加失败:", err)
            } else {
                fmt.Println("任务添加成功,id:", id)
            }
        }()
        select {
        case <-ctx.Done():
            fmt.Println("超时:", ctx.Err())
        case <-ch:
            fmt.Println("任务写入成功")
        }
    case `list`:
        ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
        defer cancel()
        data, err := service.LoadTasks(ctx, dataFile)
        if err != nil {
            fmt.Println("列出任务失败,", err)
            return
        }
        for _, itm := range data {
            fmt.Println(`id:`, itm.ID, `, content:`, itm.Content, `, done:`, itm.Done)
        }
    case `done`:
        if len(os.Args) < 3 {
            fmt.Println("请提供任务编号")
            return
        }
        id, _ := strconv.Atoi(os.Args[2])
        ctx := context.Background()
        if err := service.CompleteTask(ctx, dataFile, id); err != nil {
            fmt.Println("标记失败:", err)
        } else {
            fmt.Println("任务已完成", id)
        }
    case `del`:
        if len(os.Args) < 3 {
            return
        }
        id, _ := strconv.Atoi(os.Args[2])
        ctx := context.WithValue(context.Background(), models.IdKey, id)
        if err := service.DeleteTask(ctx, dataFile); err != nil {
            fmt.Println("删除失败:", err)
        } else {
            fmt.Println("已删除任务", id)
        }
    default:
        fmt.Println("未知命令")
    }
}

4、运行示例


$ go run main.go add "学习Go语言"
任务添加成功,id: 1762951578
任务写入成功
$ go run . add "学习人工智能"
任务添加成功,id: 1762951607
任务写入成功
$ go run . list
id: 1762951578 , content: 学习Go语言 , done: false
id: 1762951607 , content: 学习人工智能 , done: false
$ go run . done 1762951578
任务已完成 1762951578
$ go run . list
id: 1762951578 , content: 学习Go语言 , done: true
id: 1762951607 , content: 学习人工智能 , done: false
$ go run . del 1762951607
已删除任务 1762951607
$ go run . list
id: 1762951578 , content: 学习Go语言 , done: true

5、知识点回顾总结

知识点在项目中的应用
变量与函数定义任务结构、实现功能函数
切片与结构体存储任务列表
文件I/O读写data.json
JSON处理数据序列化与反序列化
错误处理error 返回与判断        
命令行参数os.Args解析用户输入

6、命令行工具有很多,最让我推荐的是眼镜蛇(cobra),它被k8s、Hugo、GitHub CLI等大型项目采用。可以自行先去学习一下,后期我会专门写一篇文章来介绍它的使用。

源码地址

1、公众号“Codee君”回复“源码”获取源码

2、pan.baidu.com/s/1B6pgLWfS… 


如果您喜欢这篇文章,请您(点赞、分享、亮爱心),万分感谢!