A lightweight command-line application for managing tasks and to-do lists, built in Go with persistent JSON storage.
Challenge: This project fulfills the roadmap.sh Task Tracker project, a beginner-level CLI development challenge.
This project demonstrates fundamental Go concepts including:
- Struct types and receiver methods
- Error handling patterns
- Package organization and encapsulation
- JSON serialization with custom field mapping
- CLI argument parsing with
os.Args - File I/O operations
task-tracker/ — Package providing task management logic:
task.go— DefinesTaskstruct and lifecycle methods (UpdateTask,ListTask)taskList.go— Manages collections viaTaskListwith CRUD operations and JSON persistencetaskState.go— Enumerates task statuses (TODO,INPROGRESS,DONE)
main.go — CLI entry point that:
- Loads tasks from
tasks.jsonon startup - Routes commands to appropriate
TaskListmethods - Persists changes to disk after mutations (not on read-only
list)
-
Exported Fields with JSON Tags: Task fields are public (
ID,Description, etc.) to enable direct JSON marshaling/unmarshaling without custom methods. This trades encapsulation for simplicity. -
UUID-Based Identification: Each task gets a unique UUID (
github.com/google/uuid) generated at creation time. Commands accept UUID strings as arguments. -
Stateless Status Sentinel:
NoStatusChange = -1is a sentinel value used in update operations to indicate "do not change status." -
Single-File Persistence: All tasks stored in
tasks.json(created in working directory). Missing file on startup is treated gracefully (empty list). -
Changed Flag Optimization: Tasks only save to disk after mutating commands (
add,update,delete,mark-*), not after read-onlylist.
cd /path/to/task-tracker-go
go build -o tasktracker .
./tasktracker <command> [args]Or run directly without building:
go run . <command> [args]./tasktracker add "Buy groceries"Creates a new task with status TODO. Generates a unique UUID.
# List all tasks
./tasktracker list
# Filter by status
./tasktracker list todo
./tasktracker list in-progress
./tasktracker list doneOutput format: <uuid>: [status] description
./tasktracker update <uuid> "Buy groceries and cook dinner"Replaces the description; status unchanged.
./tasktracker mark-in-progress <uuid>
./tasktracker mark-done <uuid>Updates task status without changing description.
./tasktracker delete <uuid>Permanently removes the task.
Tasks persist in tasks.json with this structure:
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"description": "Buy groceries",
"status": "todo",
"created_at": "2026-01-24T10:30:00Z",
"updated_at": "2026-01-24T10:30:00Z"
}
]The file is automatically created on first mutation. Loading a malformed file returns an error.
A Makefile is provided for common development tasks:
# Build the binary
make build
# Run all tests
make test
# Run tests with coverage report
make coverage
# Clean build artifacts
make clean
# Install to GOPATH/bin
make install
# Generate package documentation
make docs
# Show all available commands
make helpmake build— Compiles the binary totask-trackermake test— Runs all tests with verbose outputmake coverage— Generates coverage report (shows percentage breakdown by function)make clean— Removes binary and coverage filesmake install— Installs binary to$GOPATH/bin(available astask-tracker-go)make docs— Displays package documentation usinggo docmake help— Shows usage examples
- Field Visibility: Only exported (capitalized) struct fields are serialized by
encoding/json. Initially, lowercaseid,descriptionfields were private, causing empty JSON output. - Tag Format: JSON struct tags must use quotes:
json:"id"notjson:id. - Unmarshal Validation: Status strings (
"todo","in-progress","done") are loaded from JSON but are only validated if you add custom unmarshaling.
This CLI uses simple os.Args indexing rather than a framework like Cobra:
if len(os.Args) < 2 { return } // Check args exist
cmd := os.Args[1] // Get command
args := os.Args[2:] // Get remaining argsFor multi-word arguments, use strings.Join(args, " ").
- UUID parsing errors are caught and reported:
if err := uuid.Parse(...) - File I/O errors distinguish between missing files (OK, start empty) and other errors (fatal)
- Task lookup errors are explicit: "Task Not Found", "Update failed"
Instead of saving after every operation, we track a changed flag:
changed := false
switch cmd {
case "add":
taskList.AddNewTask(desc)
changed = true
case "list":
taskList.List(category) // changed stays false
}
if changed {
taskList.SaveToJSON(dataFile)
}This avoids disk I/O for read-only commands.
Task states are constants:
const (
TODO TaskState = iota // 0
INPROGRESS // 1
DONE // 2
)
const NoStatusChange = -1 // Sentinel for partial updatesMapping to/from JSON strings uses TaskStatus map for bidirectional lookup.
- Go type system and receiver methods
- Struct tags and reflection (for JSON encoding)
- Error handling and nil checks
- Package exports (public vs. private)
- Standard library:
encoding/json,os,fmt,uuid
This implementation fulfills all requirements from the Task Tracker project:
- ✅ Add tasks —
./tasktracker add "description" - ✅ Update tasks —
./tasktracker update <uuid> "new description" - ✅ Delete tasks —
./tasktracker delete <uuid> - ✅ Mark as in-progress —
./tasktracker mark-in-progress <uuid> - ✅ Mark as done —
./tasktracker mark-done <uuid> - ✅ List all tasks —
./tasktracker list - ✅ List by status —
./tasktracker list todo|in-progress|done
Each task persists with:
id— UUID generated at creationdescription— Task description stringstatus— One oftodo,in-progress,donecreated_at— RFC3339 timestamp on creationupdated_at— RFC3339 timestamp on update
- ✅ CLI via positional arguments — Uses
os.Argsfor command routing - ✅ JSON file storage — Single
tasks.jsonfile in current directory - ✅ Graceful missing file — Auto-creates on first mutation; starts empty if missing
- ✅ Native Go modules only — Uses only
fmt,os,encoding/json,time,uuid(external UUID library only) - ✅ Error handling — Validates UUIDs, handles missing files, reports invalid operations
- ✅ Edge cases — UUID parsing, task lookup, status validation
- Custom data file path (environment variable or flag)
- Task filtering/searching by keyword
- Priority levels or due dates
- Batch operations (mark multiple tasks)
- Colored terminal output
- Persistent timestamps as location-aware time instead of UTC