综合前面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君”回复“源码”获取源码
如果您喜欢这篇文章,请您(点赞、分享、亮爱心),万分感谢!