Level 1: CLI Task Tracker (Core Go & Testing)¶
Welcome to your first Go hands-on challenge! Your goal is to build a Command Line Interface (CLI) application to track tasks, persisted as JSON.
Features to Implement¶
Your application must support the following CLI commands:
- Add a task:
- List all tasks:
- Complete a task:
- Delete a task:
Storage & Persistence¶
- All tasks must be stored in a file named
tasks.jsonin the current working directory. - If
tasks.jsondoes not exist, the app should initialize an empty database. - The tasks must be saved dynamically after every add, complete, or delete action.
Python vs Go Cheatsheet for this Project¶
Here is how you would map typical Python patterns to Go for this project:
1. JSON Serialization¶
- Python:
- Go:
import "encoding/json" import "os" // Write data, err := json.MarshalIndent(tasks, "", " ") if err != nil { return err } err = os.WriteFile("tasks.json", data, 0644) // Read data, err := os.ReadFile("tasks.json") if err != nil { if os.IsNotExist(err) { // Handle file not existing (first run) } return err } err = json.Unmarshal(data, &tasks)
2. Classes vs Structs¶
- Python:
- Go:
3. Error Handling¶
- Python:
- Go:
Getting Started¶
- Check out task.go. We have defined the skeleton structure and function signatures.
- Check out task_test.go, which has standard tests.
- Run the tests (they will fail initially because the methods are not implemented):
- Implement the methods in task.go.
- Once your tests pass, write
main.goto handle the command-line CLI interface.
Complete Implementation (task.go)¶
package tasktracker
import (
"encoding/json"
"fmt"
"os"
"time"
)
// Task represents a task in our tracker.
// JSON tags are used to control how the struct is serialized/deserialized to/from JSON.
type Task struct {
ID int `json:"id"`
Description string `json:"description"`
Done bool `json:"done"`
CreatedAt time.Time `json:"created_at"`
}
// TaskManager handles the management and persistence of tasks.
type TaskManager struct {
FilePath string
}
// NewTaskManager creates a new instance of TaskManager using the provided file path for storage.
func NewTaskManager(filePath string) *TaskManager {
return &TaskManager{
FilePath: filePath,
}
}
// LoadTasks reads and parses tasks from the JSON storage file.
// If the file does not exist, it should return an empty slice and nil error.
func (tm *TaskManager) LoadTasks() ([]Task, error) {
data, err := os.ReadFile(tm.FilePath)
if err != nil {
if os.IsNotExist(err) {
return []Task{}, nil
}
return nil, err
}
var tasks []Task
if err := json.Unmarshal(data, &tasks); err != nil {
return nil, err
}
return tasks, nil
}
// SaveTasks serializes the list of tasks to JSON and writes them to the storage file.
func (tm *TaskManager) SaveTasks(tasks []Task) error {
data, err := json.MarshalIndent(tasks, "", " ")
if err != nil{
return err
}
fmt.Print(tasks)
os.WriteFile(tm.FilePath, data, 0644)
return nil
// TODO: Implement saving tasks to tm.FilePath.
// Hint: Use json.MarshalIndent (or json.Marshal) to encode the slice of tasks into JSON bytes.
// Use os.WriteFile to write the bytes to the file.
}
// AddTask creates a new task, assigns it a unique ID (incremented from the max existing ID),
// appends it to the task list, and saves it.
func (tm *TaskManager) AddTask(description string) (Task, error) {
tasks, err := tm.LoadTasks()
if err != nil {
return Task{}, err
}
newID := len(tasks) + 1
newTask := Task{
ID: newID,
Description: description,
Done: false,
CreatedAt: time.Now(),
}
tasks = append(tasks, newTask)
tm.SaveTasks(tasks)
return newTask, nil
// TODO: Implement adding a task.
// 1. Load existing tasks.
// 2. Find the highest current ID and add 1 to get the new task's ID (start with 1 if no tasks exist).
// 3. Create a new Task struct (set ID, Description, Done = false, CreatedAt = time.Now()).
// 4. Append it to the slice.
// 5. Save the updated list.
// 6. Return the newly created task.
}
// ListTasks returns all tasks.
func (tm *TaskManager) ListTasks() ([]Task, error) {
tasks, err := tm.LoadTasks()
if err != nil{
return nil, err
}
// TODO: Implement listing tasks.
// Hint: Just load and return the tasks.
return tasks, nil
}
// CompleteTask marks the task with the given ID as done and saves the tasks list.
// If the task does not exist, it should return an error.
func (tm *TaskManager) CompleteTask(id int) error {
tasks , err := tm.LoadTasks()
if err !=nil{
return err
}
for i,task:= range tasks{
if task.ID == id{
tasks[i].Done = true
tm.SaveTasks(tasks)
return nil
}
}
return fmt.Errorf("task with ID %d not found", id)
// TODO: Implement completing a task.
// 1. Load existing tasks.
// 2. Find the task with the matching ID.
// 3. If found, set Done = true. If not found, return an error.
// 4. Save the updated list.
}
// DeleteTask removes the task with the given ID from the tasks list and saves it.
// If the task does not exist, it should return an error.
func (tm *TaskManager) DeleteTask(id int) error {
tasks, err := tm.LoadTasks()
if err !=nil {
return err
}
for i,task := range tasks{
if task.ID == id{
tasks = append(tasks[:i], tasks[i+1:]...)
tm.SaveTasks(tasks)
return nil
}
}
// TODO: Implement deleting a task.
// 1. Load existing tasks.
// 2. Find the task with the matching ID. If not found, return an error.
// 3. Remove the task from the slice (hint: use append(slice[:i], slice[i+1:]...) to remove index i).
// 4. Save the updated list.
return fmt.Errorf("Task ID %d not present",id)
}