Skip to main content

Command Palette

Search for a command to run...

Build a CLI using Cobra in Golang

Published
20 min read
Build a CLI using Cobra in Golang

Task tracker is a project used to track and manage your tasks. In this task, you will build a simple command line interface (CLI) to track what you need to do, what you have done, and what you are currently working on. This project will help you practice your programming skills, including working with the filesystem, handling user inputs, and building a simple CLI application.

https://roadmap.sh/projects/task-tracker

Project URL : https://github.com/pranav767/GO_PROJECTS/tree/main/task-tracker

Requirements

The application should run from the command line, accept user actions and inputs as arguments, and store the tasks in a JSON file. The user should be able to:

  • Add, Update, and Delete tasks

  • Mark a task as in progress or done

  • List all tasks

  • List all tasks that are done

  • List all tasks that are not done

  • List all tasks that are in progress

Cobra CLI docs
https://cobra.dev/docs/

https://cobra.dev/docs/tutorials/getting-started/

Install Cobra

$ go get -u github.com/spf13/cobra@latest
$ go install github.com/spf13/cobra-cli@latest

Create your application

$ cobra-cli init task-tracker
Your Cobra application is ready at task-tracker
Add commands to your application by using the `add` command

This creates a new directory called task-tracker with a complete Go module and basic Cobra application structure. Let’s explore what was created:

$ cd task-tracker
$ ls -la
total 16
drwxr-xr-x   6 jinx  jinx     192 Aug 10 12:00 .
drwxr-xr-x   3 jinx  jinx     96 Aug 10 12:00 ..
drwxr-xr-x   3 jinx  jinx     96 Aug 10 12:00 cmd
-rw-r--r--   1 jinx  jinx     87 Aug 10 12:00 go.mod
-rw-r--r--   1 jinx  jinx     155 Aug 10 12:00 go.sum
-rw-r--r--   1 jinx  jinx     615 Aug 10 12:00 main.go

Test your base application

$ go run main.go
A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.

$ go run main.go --help
A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.
Usage:
task-tracker [flags]
Flags:
-h, --help   help for task-tracker

Add your first command

jinx@MSI:~/go/GO_PROJECTS/task-tracker$ cobra-cli --help
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.

Usage:
  cobra-cli [command]

Available Commands:
  add         Add a command to a Cobra Application
  completion  Generate the autocompletion script for the specified shell
  help        Help about any command
  init        Initialize a Cobra Application

Flags:
  -a, --author string    author name for copyright attribution (default "YOUR NAME")
      --config string    config file (default is $HOME/.cobra.yaml)
  -h, --help             help for cobra-cli
  -l, --license string   name of license for the project
      --viper            use Viper for configuration

Use "cobra-cli [command] --help" for more information about a command.
$ cobra-cli add add
add created at cmd/add.go
$ go run main.go add
Add from your new Cobra CLI application!

This project involves implementing basic CRUD operations, Understanding the structure of our project:

add.go : adds a todo item
delete.go: deletes a todo item
list.go: list all todo items
markDone.go: marks an item as done
markInProgress.go: marks an item as in progress
update.go: updates an item's description

Add all the above files using command cobra-cli add <task-name>, this will automatically add files to the cmd folder, these files will just have the function calls to do a certain task.

Add a new file under folder tasks , storage.go this file will have logic to do actual tasks , i.e. function defination

Remember root.go is the main entrypoint for cli, here all child commands like add, delete,list,update etc are executed

Let us build this project bit by bit,

Since this is just a small project to learn CRUD operations in golang we’ll just have a local json file which will hold all the data, let us start with inserting a task that is add.go, if we try to execute this file it will just say add.go called, we’ll have to make few tweaks to the file

package cmd

import (
    "fmt"
    "github.com/spf13/cobra"
    "task-tracker/tasks"
    "os"
)

// addCmd represents the add command
var addCmd = &cobra.Command{
    Use:   "add",
    Short: "Add a new task",
    Args:  cobra.MinimumNArgs(1), // Ensure at least one argument is provided
    Long: `Adds a new task to existing list. For example:

./task-tracker add "Buy groceries"`,
    Run: func(cmd *cobra.Command, args []string) {
        task, err := tasks.AddTask(args[0])
        if err != nil {
            fmt.Println("Error adding task:", err)
               os.Exit(1)
          }
        fmt.Println("Task added successfully:", task.ID)
    },
}

func init() {
    rootCmd.AddCommand(addCmd)
}

Update the Short & Long args, these are just short & long description of the command, for adding a task , their must always be a description of the task that is one argument should always be there with task-tracker add <description>, Args: cobra.MinimumNArgs(1), this does the job

Run: func(cmd *cobra.Command,args []string){, this will be the entry point for the cli command add.

  • task, err := tasks.AddTask(args[0]): Calls the AddTask function with the first argument (the task description) and returns the created task and any error.

  • if err != nil this calls os.Exit,which exits the program if there are errors.

  • AddTask will be a function defined in storage.go, which will have logic to add todo item.

Similar to this we’ll have

  • delete.go : calls DeleteTask(id) function in tasks package. (deletes a task using ID)

  • list.go: calls LoadTasks() function in tasks package. (lists all tasks)

  • markDone.go: calls markDone() function in tasks packge.(mark a task as Done using ID)

  • markInProgress.go : calls markTaskInProgress() function in tasks package. (marks a task as In Progress using ID)

  • update.go : calls UpdateTask() function in tasks package. (update the description of a task using ID)

Remember root.go, is the parent file for all other go file command calls:

package cmd

import (
    "os"
    "github.com/spf13/cobra"
)

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
    Use:   "task-tracker",
    Short: "Task Tracker CLI",
    Long: `Task Tracker CLI is a command-line tool to manage your tasks efficiently.
It allows you to add, delete, and manage tasks with ease. You can mark tasks as done, in-progress, or to-do.
For example:

./task-tracker add "Buy groceries"
./task-tracker delete 1
./task-tracker mark-done 2
./task-tracker list
./task-tracker list done
./task-tracker list todo
./task-tracker list in-progress`,
}

// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
    err := rootCmd.Execute()
    if err != nil {
        os.Exit(1)
    }
}

func init() {
    rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

Thinking about logic for adding a task item, we’ll first have to create a structure which contains key value pairs for tasks, then we’ll have a json file which stores the actual data, we’ll have to read/load the json file everytime, update with new data i.e. add new tasks/update existing tasks/delete any specific tasks , this is how the shared logic would look like in storage.json

// Shared task logic

package tasks
import (
    "fmt"
    "encoding/json"
    "os"
    "time"
)

// structure of the task storage file
type Task struct {
    ID                  int    `json:"id"`
    Description        string `json:"description"`
    Status          string `json:"status"`
    CreatedAt        string `json:"created_at"`
    UpdatedAt        string `json:"updated_at"`
}

// there will be a file to store tasks
const storageFile = "tasks.json"

// LoadTasks loads tasks from the storage file
func LoadTasks() ([]Task, error) {
    data, err := os.ReadFile(storageFile)
    if err != nil {
        // If the file does not exist, return an empty slice
        if os.IsNotExist(err) {
            return []Task{}, nil
        }
        fmt.Println("Error reading tasks file:", err)
        return nil, err
    }
    //  If the file is empty, return an empty slice
    if len(data) == 0 {
        return []Task{}, nil // Return empty slice if file is empty
    }
    var tasks []Task
    err = json.Unmarshal(data, &tasks)
    if err != nil {
        fmt.Println("Error unmarshalling tasks:", err)
        return nil, err
    }
    return tasks,nil
}

// AddTask adds a new task to the storage file
func AddTask(Description string) (Task, error) {
    // get existing tasks
    tasks, err := LoadTasks()
    if err != nil {
        return Task{}, err
    }
    // Create a new task
    newID := 1
    for _, t := range tasks {
        if t.ID >=newID {
            newID = t.ID +1
        }
    }
    currentTime := time.Now().Format(time.RFC3339)
    task := Task{
        ID:         newID,
        Description: Description,
        Status:     "TO-DO",
        CreatedAt:  currentTime,
        UpdatedAt: currentTime,
    }
    tasks = append(tasks, task)
    // We have entire tasks list, rewrite this to the file
    data, err := json.MarshalIndent(tasks, "", "  ")
    if err != nil {
        return Task{}, err
    }
    err = os.WriteFile(storageFile, data, 0644)
    if err != nil {
        return Task{}, err
    }
    return task, nil
}

// DeleteTask deletes a task by ID
func DeleteTask(id int) error {
    tasks, err := LoadTasks()
    newTasks := make([]Task, 0, len(tasks))
    for _,t := range tasks{
        if t.ID !=id {
            newTasks = append(newTasks, t)
        }
    }
    data, err := json.MarshalIndent(newTasks, "", "  ")
    if err != nil {
        return err
    }
    err = os.WriteFile(storageFile, data, 0644)
    if err != nil {
        return err
    }
    return nil
}

// Update existing task by ID
func UpdateTask(id int, Description string) error {
    tasks, err := LoadTasks()
    newTasks := make([]Task, 0, len(tasks))
    for _,t := range tasks{
        if t.ID !=id {
            newTasks = append(newTasks, t)
        } else {
            // Update the task
            t.Description = Description
            t.UpdatedAt = time.Now().Format(time.RFC3339)
            newTasks = append(newTasks, t)
        }
    }
    data, err := json.MarshalIndent(newTasks, "", "  ")
    if err != nil {
        return err
    }
    err = os.WriteFile(storageFile, data, 0644)
    if err != nil {
        return err
    }
    return nil
}

//Mark a task as done by ID
func MarkTaskDone(id int) error {
    tasks, err := LoadTasks()
    newTasks := make([]Task, 0, len(tasks))
    for _,t := range tasks{
        if t.ID !=id {
            newTasks = append(newTasks, t)
        } else {
            // Update the task as Done
            t.Status = "DONE"
            t.UpdatedAt = time.Now().Format(time.RFC3339)
            newTasks = append(newTasks, t)
        }
    }
    data, err := json.MarshalIndent(newTasks, "", "  ")
    if err != nil {
        return err
    }
    err = os.WriteFile(storageFile, data, 0644)
    if err != nil {
        return err
    }
    return nil
} 

//Mark a task as done by ID
func MarkTaskInProgress(id int) error {
    tasks, err := LoadTasks()
    newTasks := make([]Task, 0, len(tasks))
    for _,t := range tasks{
        if t.ID !=id {
            newTasks = append(newTasks, t)
        } else {
            // Update the task as Done
            t.Status = "IN-PROGRESS"
            t.UpdatedAt = time.Now().Format(time.RFC3339)
            newTasks = append(newTasks, t)
        }
    }
    data, err := json.MarshalIndent(newTasks, "", "  ")
    if err != nil {
        return err
    }
    err = os.WriteFile(storageFile, data, 0644)
    if err != nil {
        return err
    }
    return nil
}

Let’s break down each line and try to understand working of this file,

package tasks
import (
    "fmt"
    "encoding/json"
    "os"
    "time"
)

Since this file is in inside a tasks folder , we’ll need to say package tasks, then importing certain packages which will be useful later, fmt(print stuff), encoding/json (read/write/load json files), os (use os commands like exit), time (get current time), can be found here: https://pkg.go.dev/
Structure of a Task item will have ID(int), Description, Status, CreatedAt & UpdatedAt. We’ll also have a constant variable for the filename.

// structure of the task storage file
type Task struct {
    ID                  int    `json:"id"`
    Description        string `json:"description"`
    Status          string `json:"status"`
    CreatedAt        string `json:"created_at"`
    UpdatedAt        string `json:"updated_at"`
}

// there will be a file to store tasks
const storageFile = "tasks.json"

For playing around with json, it is important to understand marshalling & unmarshalling in golang.

Marshalling (Struct → JSON)

When you call

data, err := json.MarshalIndent(tasks, "", "  ")

You’re telling Go:

“Take my tasks slice (a Go object) and convert it into a JSON-formatted byte array.”

Example:

tasks := []Task{
  {ID: 1, Description: "Buy milk", Done: false},
}
data, _ := json.MarshalIndent(tasks, "", "  ")
fmt.Println(string(data))

Output:

[
  {
    "ID": 1,
    "Description": "Buy milk",
    "Done": false
  }
]

You then write this data to a file:

os.WriteFile("tasks.json", data, 0644)

Unmarshalling (JSON → Struct)

When your CLI restarts, you need to load that saved data back into Go structs.

data, err := os.ReadFile("tasks.json")
json.Unmarshal(data, &tasks)

Here, Go reads the JSON bytes and fills your tasks slice with values.

Example:

var tasks []Task
data := []byte(`[{"ID":1,"Description":"Buy milk","Done":false}]`)
json.Unmarshal(data, &tasks)
fmt.Println(tasks[0].Description) // Output: Buy milk

When you’re building your Task Tracker, you need to store tasks (Go structs) in a file (tasks.json) and later load them back into Go structs when your CLI runs again.
That’s exactly where marshalling and unmarshalling come in.

Let us first try to read/load the jsonfile.

LoadTasks

// LoadTasks loads tasks from the storage file
func LoadTasks() ([]Task, error) {
    data, err := os.ReadFile(storageFile)
    if err != nil {
        // If the file does not exist, return an empty slice
        if os.IsNotExist(err) {
            return []Task{}, nil
        }
        fmt.Println("Error reading tasks file:", err)
        return nil, err
    }
    //  If the file is empty, return an empty slice
    if len(data) == 0 {
        return []Task{}, nil // Return empty slice if file is empty
    }
    var tasks []Task
    err = json.Unmarshal(data, &tasks)
    if err != nil {
        fmt.Println("Error unmarshalling tasks:", err)
        return nil, err
    }
    return tasks,nil
}

function loadtasks returns a structure task i.e. items from the file and a err (if there are any errors during execution of the file)

  • data, err := os.ReadFile(storageFile) : reads the file using os package, data stores the actual data from the file ,next if err != nil { if there are some errors we’ll have to return the errors or if the file does not exists we have to intialize the data with a empty task structure which is done in line if os.IsNotExist(err) which returns []Task{}, nil empty task structure and nil as error, if the err is not nil due to empty file then there is an error while reading the file, so return the error here.

  • Next would be a case if the file is empty check is done via if len(data) == 0 { in case also we’ll return a empty task structure with nil error.(return []Task{}, nil)

  • After reading/loading the data from the json file we’ll decode/unmarshal the data using json package

  • var tasks []Task: Declares a variable tasks as a slice of Task structs. In Go, a slice is a reference type (pointer to an underlying array).

  • err = json.Unmarshal(data, &tasks): Uses the address-of operator (&tasks) to pass a pointer to the tasks slice to json.Unmarshal. This allows json.Unmarshal to fill the slice with decoded data from data.

  • Next we handle error gracefully return nil, err: Returns nil and the error if unmarshalling fails.

  • return tasks,nil: Returns the populated tasks slice and nil error if successful.

Using this function can list all tasks, but we also have to give filtered list like :

./task-tracker list done
./task-tracker list todo
./task-tracker list in-progress

To add sub commands ,do

  • cobra-cli add done --parent list

  • cobra-cli add todo --parent list

  • cobra-cli add in-progress --parent list

This will add the subflags in list.go file:

/*
Copyright © 2025 Pranav <pranavppatil767@gmail.com>

*/
package cmd

import (
    "fmt"
    "task-tracker/tasks"
    "github.com/spf13/cobra"
)

// listCmd represents the list command
var listCmd = &cobra.Command{
    Use:   "list",
    Short: "List all tasks",
    Long: `List all tasks. For example:

./task-tracker list`,
    Run: func(cmd *cobra.Command, args []string) {
        task, err := tasks.LoadTasks()
        if err != nil {
            fmt.Println("Error loading tasks:", err)
        }
        for _, t := range task {
            fmt.Printf("ID: %d, Description: %s, Status: %s, Created At: %s, Updated At: %s\n", t.ID, t.Description, t.Status, t.CreatedAt, t.UpdatedAt)
        }
    },
}

var listDoneCmd = &cobra.Command{
    Use:   "done",
    Short: "List all tasks that are marked as done",
    Long: `A Subcommand for list which lists out all tasks marked as done For example:

./task-tracker list done`,
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("list done called")
        task,err := tasks.LoadTasks()
        if err != nil {
            fmt.Println("Error loading tasks:", err)
        }
        for _, t := range task {
            if t.Status == "DONE" {
                fmt.Printf("ID: %d, Description: %s, Status: %s, Created At: %s, Updated At: %s\n",t.ID, t.Description, t.Status, t.CreatedAt, t.UpdatedAt)
            }
        }
    },
}

var listToDoCmd = &cobra.Command{
    Use:   "todo",
    Short: "List all tasks that are marked as To-Do",
    Long: `A Subcommand for list which lists out all tasks marked as To-Do For example:

./task-tracker list todo`,
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("list todo called")
        task,err := tasks.LoadTasks()
        if err != nil {
            fmt.Println("Error loading tasks:", err)
        }
        for _, t := range task {
            if t.Status == "To-DO" {
                fmt.Printf("ID: %d, Description: %s, Status: %s, Created At: %s, Updated At: %s\n",t.ID, t.Description, t.Status, t.CreatedAt, t.UpdatedAt)
            }
        }
    },
}

var listInProgressCmd = &cobra.Command{
    Use:   "in-progress",
    Short: "List all tasks that are marked as In-Progress",
    Long: `A Subcommand for list which lists out all tasks marked as IN-PROGRESS. For example:

./task-tracker list in-progress`,
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("list in-progress called")
        task,err := tasks.LoadTasks()
        if err != nil {
            fmt.Println("Error loading tasks:", err)
        }
        for _, t := range task {
            if t.Status == "IN-PROGRESS" {
                fmt.Printf("ID: %d, Description: %s, Status: %s, Created At: %s, Updated At: %s\n",t.ID, t.Description, t.Status, t.CreatedAt, t.UpdatedAt)
            }
        }
    },
}

func init() {
    rootCmd.AddCommand(listCmd)
    listCmd.AddCommand(listDoneCmd)
    listCmd.AddCommand(listToDoCmd)
    listCmd.AddCommand(listInProgressCmd)
}

The only difference between just a list command and list done command will be we’ll iterate through each task in the loaded list and use a for loop to get only tasks which are in status DONE/TODO/IN-PROGRESS, and print details for those tasks.

AddTask

After loading existing data we will try to add a new task using function AddTask, for adding a task we’ll have to create a unique ID and add the description from user input.

// AddTask adds a new task to the storage file
func AddTask(Description string) (Task, error) {
    // get existing tasks
    tasks, err := LoadTasks()
    if err != nil {
        return Task{}, err
    }
    // Create a new task
    newID := 1
    for _, t := range tasks {
        if t.ID >=newID {
            newID = t.ID +1
        }
    }
    currentTime := time.Now().Format(time.RFC3339)
    task := Task{
        ID:         newID,
        Description: Description,
        Status:     "TO-DO",
        CreatedAt:  currentTime,
        UpdatedAt: currentTime,
    }
    tasks = append(tasks, task)
    // We have entire tasks list, rewrite this to the file
    data, err := json.MarshalIndent(tasks, "", "  ")
    if err != nil {
        return Task{}, err
    }
    err = os.WriteFile(storageFile, data, 0644)
    if err != nil {
        return Task{}, err
    }
    return task, nil
}

AddTask takes in description as a string and returns the task struct & err if any present.

  • Loads existing tasks using LoadTasks() function, if loading fails return empty task and the error

  • Next the for loop determines a unique ID for the new task by finding the highest existing ID and adding 1.

  • Get the current time in RFC3339 format for timestamps.

  • Create a new Task structure with the provided description, status as TO-DO and timestamps.

  • Appends the new task to the list of tasks.

  • Serializes the updated task list to JSON using json.MarshalIndent

  • If serialization fails, returns an empty Task and the error.

  • Write the JSON data back to the storage file , if writing fails returns and empty task and the error.

  • Returns the newly created task & nil error if succesfull.

DeleteTask

Delete task deletes a task using ID

func DeleteTask(id int) error {
    tasks, err := LoadTasks()
    newTasks := make([]Task, 0, len(tasks))
    for _,t := range tasks{
        if t.ID !=id {
            newTasks = append(newTasks, t)
        }
    }
    data, err := json.MarshalIndent(newTasks, "", "  ")
    if err != nil {
        return err
    }
    err = os.WriteFile(storageFile, data, 0644)
    if err != nil {
        return err
    }
    return nil
}

Deletetask takes input as a ID and returns any errors if found while deleting a task

  • Loads existing tasks using LoadTasks() function, if loading fails return empty task and the error

  • Creates a new slice newTasks to hold tasks that are not being deleted.([]Task : is a slice of task structure)

  • Iterates through each task in the loaded list, appends tasks to newTasks only if their ID does not match the given id

  • Serialized the filtered list newTasks to JSON using json.MarshalIndent, if serialization fails returns the error

  • Writes the updates JSON data back to storageFile, if writing fails returns the error.

  • Returns nil if the deletion & file update are successfull.

UpdateTask

To update a task, user will provide a ID of the task and the new description, we’ll have to update the description & UpdatedAt timestamp.

func UpdateTask(id int, Description string) error {
    tasks, err := LoadTasks()
    newTasks := make([]Task, 0, len(tasks))
    for _,t := range tasks{
        if t.ID !=id {
            newTasks = append(newTasks, t)
        } else {
            // Update the task
            t.Description = Description
            t.UpdatedAt = time.Now().Format(time.RFC3339)
            newTasks = append(newTasks, t)
        }
    }
    data, err := json.MarshalIndent(newTasks, "", "  ")
    if err != nil {
        return err
    }
    err = os.WriteFile(storageFile, data, 0644)
    if err != nil {
        return err
    }
    return nil
}

Similar to addtask:

  • Loads existing tasks using LoadTasks() function, if loading fails return empty task and the error

  • Creates a new slice newTasks of struct task to hold all tasks.

  • Iterates through each task in the loaded list, appends tasks to newTasks only if their ID does not match the given id, this will keep original task list as it was, for the matching ID we’ll update the Description and the UpdatedAt timestamp.

  • Serialized the filtered list newTasks to JSON using json.MarshalIndent, if serialization fails returns the error

  • Writes the updates JSON data back to storageFile, if writing fails returns the error.

  • Returns nil if the updating task & file update are successfull.

MarkTaskDone

MarkTaskDone this function takes a task id and updates the status from In-progress to Done, we’ll have to update 2 fields in the tasks structure : status & UpdatedAt.

func MarkTaskDone(id int) error {
    tasks, err := LoadTasks()
    newTasks := make([]Task, 0, len(tasks))
    for _,t := range tasks{
        if t.ID !=id {
            newTasks = append(newTasks, t)
        } else {
            // Update the task as Done
            t.Status = "DONE"
            t.UpdatedAt = time.Now().Format(time.RFC3339)
            newTasks = append(newTasks, t)
        }
    }
    data, err := json.MarshalIndent(newTasks, "", "  ")
    if err != nil {
        return err
    }
    err = os.WriteFile(storageFile, data, 0644)
    if err != nil {
        return err
    }
    return nil
}
  • Loads existing tasks using LoadTasks() function, if loading fails return empty task and the error

  • Creates a new slice newTasks of struct task to hold all tasks.

  • Iterates through each task in the loaded list, appends tasks to newTasks only if their ID does not match the given id, this will keep original task list as it was, for the matching ID we’ll update the status as Done and the UpdatedAt timestamp with currenttimestamp.

  • Serialized the filtered list newTasks to JSON using json.MarshalIndent, if serialization fails returns the error

  • Writes the updates JSON data back to storageFile, if writing fails returns the error.

  • Returns nil if the updating task & file update are successfull.

MarkTaskInProgress

MarkTaskInProgress this function takes a task id and updates the status from In-progress to Done, we’ll have to update 2 fields in the tasks structure : status & UpdatedAt.

func MarkTaskInProgress(id int) error {
    tasks, err := LoadTasks()
    newTasks := make([]Task, 0, len(tasks))
    for _,t := range tasks{
        if t.ID !=id {
            newTasks = append(newTasks, t)
        } else {
            // Update the task as Done
            t.Status = "IN-PROGRESS"
            t.UpdatedAt = time.Now().Format(time.RFC3339)
            newTasks = append(newTasks, t)
        }
    }
    data, err := json.MarshalIndent(newTasks, "", "  ")
    if err != nil {
        return err
    }
    err = os.WriteFile(storageFile, data, 0644)
    if err != nil {
        return err
    }
    return nil
}
  • Loads existing tasks using LoadTasks() function, if loading fails return empty task and the error

  • Creates a new slice newTasks of struct task to hold all tasks.

  • Iterates through each task in the loaded list, appends tasks to newTasks only if their ID does not match the given id, this will keep original task list as it was, for the matching ID we’ll update the status as In-progress and the UpdatedAt timestamp with currenttimestamp.

  • Serialized the filtered list newTasks to JSON using json.MarshalIndent, if serialization fails returns the error

  • Writes the updates JSON data back to storageFile, if writing fails returns the error.

  • Returns nil if the updating task & file update are successfull.

We now have all things in place, each command like add,list,delete have a file inside cmd folder, which will have logic for cli command calls and the actual logic for CRUD operations on the tasks will be in tasks/storage.go.

Moment of truth

let’s test this out now.

jinx@MSI:~/go/GO_PROJECTS/task-tracker$ go build -o task-tracker
jinx@MSI:~/go/GO_PROJECTS/task-tracker$ ./task-tracker list
jinx@MSI:~/go/GO_PROJECTS/task-tracker$ ./task-tracker add "Buy groceries"
Task added successfully: 1

jinx@MSI:~/go/GO_PROJECTS/task-tracker$ ./task-tracker list
ID: 1, Description: Buy groceries, Status: TO-DO, Created At: 2025-08-21T00:35:46+05:30, Updated At: 2025-08-21T00:35:46+05:30

jinx@MSI:~/go/GO_PROJECTS/task-tracker$ ./task-tracker add "Buy PS5"
Task added successfully: 2

jinx@MSI:~/go/GO_PROJECTS/task-tracker$ ./task-tracker list
ID: 1, Description: Buy groceries, Status: TO-DO, Created At: 2025-08-21T00:35:46+05:30, Updated At: 2025-08-21T00:35:46+05:30
ID: 2, Description: Buy PS5, Status: TO-DO, Created At: 2025-08-21T00:38:39+05:30, Updated At: 2025-08-21T00:38:39+05:30

jinx@MSI:~/go/GO_PROJECTS/task-tracker$ ./task-tracker update 1 "Buy groceries and cook dinner"
Task Updated successfully: 1

jinx@MSI:~/go/GO_PROJECTS/task-tracker$ ./task-tracker list
ID: 1, Description: Buy groceries and cook dinner, Status: TO-DO, Created At: 2025-08-21T00:35:46+05:30, Updated At: 2025-08-21T00:39:02+05:30
ID: 2, Description: Buy PS5, Status: TO-DO, Created At: 2025-08-21T00:38:39+05:30, Updated At: 2025-08-21T00:38:39+05:30

jinx@MSI:~/go/GO_PROJECTS/task-tracker$ ./task-tracker mark-in-progress 2
Task Updated successfully: 2

jinx@MSI:~/go/GO_PROJECTS/task-tracker$ ./task-tracker list
ID: 1, Description: Buy groceries and cook dinner, Status: TO-DO, Created At: 2025-08-21T00:35:46+05:30, Updated At: 2025-08-21T00:39:02+05:30
ID: 2, Description: Buy PS5, Status: IN-PROGRESS, Created At: 2025-08-21T00:38:39+05:30, Updated At: 2025-08-21T00:39:35+05:30

jinx@MSI:~/go/GO_PROJECTS/task-tracker$ ./task-tracker list todo
list todo called
ID: 1, Description: Buy groceries and cook dinner, Status: TO-DO, Created At: 2025-08-21T00:35:46+05:30, Updated At: 2025-08-21T00:39:02+05:30

jinx@MSI:~/go/GO_PROJECTS/task-tracker$ ./task-tracker delete 1
Task deleted successfully: 1
jinx@MSI:~/go/GO_PROJECTS/task-tracker$ ./task-tracker list
ID: 2, Description: Buy PS5, Status: IN-PROGRESS, Created At: 2025-08-21T00:38:39+05:30, Updated At: 2025-08-21T00:39:35+05:30

Works as expected 🥳🥳🥳

Ciao 👋

More from this blog

Pranav's Blog

27 posts