On this page

Final Project: CLI Task Manager with HTTP API

25 min read TextCh. 5 — Go in Production

Final Project: CLI Task Manager with HTTP API

Congratulations on reaching the final lesson! You have covered the full breadth of modern Go — from basic types and control flow to structs, interfaces, goroutines, channels, error handling, modules, and HTTP. Now it is time to consolidate everything by building a real, complete project from scratch.

What You Will Build

A task manager that operates in two modes:

  1. CLI mode: interact from the command line (list, create, complete, delete)
  2. HTTP server mode: exposes a REST API on localhost:8080

Data is persisted to a JSON file, concurrency is protected with sync.RWMutex, and the server shuts down gracefully by capturing OS signals.

Project Architecture

go-tasks/
├── go.mod
└── main.go          ← everything in one file for this project

The design follows these Go principles:

  • Repository interface: decouples business logic from the storage implementation
  • JSONRepo struct: concrete implementation with file-based persistence
  • Thread-safe concurrency: sync.RWMutex protects the task map
  • Graceful shutdown: the HTTP server listens for OS signals to close cleanly
  • Explicit error handling: every function returns error; sentinel errors enable comparison with errors.Is

Applied Concepts

1. Interfaces for Decoupling

The Repository interface defines the contract that any storage implementation must fulfill. The CLI and server work exclusively with this interface:

type Repository interface {
    Create(title, description string, priority int) (*Task, error)
    List() ([]*Task, error)
    GetByID(id int) (*Task, error)
    Update(task *Task) error
    Delete(id int) error
}

To add PostgreSQL storage, you would only need to create a PostgresRepo struct implementing the same 5 methods.

2. Concurrency with sync.RWMutex

The HTTP server receives multiple concurrent requests. The task map must be protected:

// Multiple concurrent reads are allowed
func (r *JSONRepo) List() ([]*Task, error) {
    r.mu.RLock()        // multiple goroutines can read simultaneously
    defer r.mu.RUnlock()
    // ...
}

// Only one write at a time
func (r *JSONRepo) Create(...) (*Task, error) {
    r.mu.Lock()         // blocks everything: readers and writers
    defer r.mu.Unlock()
    // ...
}

3. ServeMux Pattern Matching (Go 1.22+)

mux.HandleFunc("GET /api/tasks", list)
mux.HandleFunc("POST /api/tasks", create)
mux.HandleFunc("GET /api/tasks/{id}", getByID)
mux.HandleFunc("DELETE /api/tasks/{id}", delete)

The HTTP method in the pattern ensures that GET /api/tasks and POST /api/tasks route to different handlers automatically.

4. Graceful Shutdown with Channels and Select

signals := make(chan os.Signal, 1)
signal.Notify(signals, os.Interrupt, syscall.SIGTERM)
errCh := make(chan error, 1)

go func() {
    errCh <- srv.ListenAndServe()
}()

select {
case err := <-errCh:
    // Server terminated due to an error
case <-signals:
    // Ctrl+C or SIGTERM: shut down gracefully
    srv.Shutdown(ctx)
}

How to Run the Project

# Initialize the module
go mod init github.com/yourusername/go-tasks

# List tasks
go run main.go list

# Create a task
go run main.go create --title "Study Go" --desc "Complete the course" --priority 3

# Complete a task
go run main.go complete 1

# Start the HTTP server
go run main.go server

# In another terminal, test the API
curl http://localhost:8080/api/tasks
curl -X POST http://localhost:8080/api/tasks \
  -H "Content-Type: application/json" \
  -d '{"title":"New task","priority":2}'
curl http://localhost:8080/api/tasks/1
curl -X DELETE http://localhost:8080/api/tasks/1

Possible Extensions

Once the base project works, you can extend it with:

  1. List filters: GET /api/tasks?status=pending&priority=3
  2. Basic authentication: middleware that verifies an API key in the header
  3. Pagination: GET /api/tasks?page=2&per_page=10
  4. Due dates: a DueAt *time.Time field in Task
  5. Notifications: a goroutine that checks for overdue tasks and notifies
  6. SQLite: implement SQLiteRepo with database/sql and the modernc.org/sqlite driver
  7. Tests: InMemoryRepo for unit tests without files

What You Learned in This Course

With this project you complete your mastery of essential Go:

  • Static, strong, expressive type system
  • Functions with multiple returns and the (value, error) pattern
  • Structs, methods, and implicit interfaces
  • Slices and maps with their specific idioms
  • Safe pointers without pointer arithmetic
  • Explicit error handling with errors.Is and errors.As
  • Goroutines — millions possible at 2-4 KB each
  • Channels for safe communication between goroutines
  • Concurrency patterns: worker pool, fan-in/out, pipeline
  • context.Context for propagated cancellation
  • Module and package system
  • REST APIs with net/http and Go 1.22+ ServeMux

Go is the language of modern infrastructure. Docker, Kubernetes, Terraform, Prometheus — all born in Go. With this course you have the foundation to build tools of the same caliber.

The natural next steps are to explore the Go ecosystem: gorm or sqlx for databases, gin or chi for advanced routing, and testify for more expressive testing.

The Repository interface decouples business logic from storage
The CLI and HTTP server work exclusively with the Repository interface, not with JSONRepo directly. This means you can create a PostgreSQL, SQLite, or in-memory implementation, and the rest of the code works without changes. This is the power of interface polymorphism in Go.
sync.RWMutex protects concurrent map access in the HTTP server
The HTTP server can receive multiple simultaneous requests, each in its own goroutine. Without the RWMutex, multiple goroutines would read and write the task map concurrently — a data race. RLock() allows concurrent reads, Lock() guarantees exclusive writes.