“Speed means nothing if your data disappears.”
Up to this point, our Go expense tracker can log new expenses, filter by category, and display summaries.
But a tracker that forgets everything when you close the terminal isn’t really a tracker — it’s a calculator with amnesia.
In this chapter, we’ll make it remember — by saving data locally, in a format that’s simple, reliable, and future-proof.
This is the Go way: no magic, no dependencies, just solid engineering.
4.1 The Goal
Save expenses in a way that’s:
- Readable by both humans and programs.
- Persistent across sessions.
- Simple enough to extend later without breaking things.
The perfect fit? A plain CSV file — easy to read, easy to open in Excel, Google Sheets, or even Notepad.
4.2 The Go Philosophy: “Straightforward, not fragile”
Go was built for production systems.
That means everything — even file handling — follows one principle: do one thing well, and make it predictable.
With Go’s standard os and encoding/csv packages, you can handle all your data storage needs with just a few clean functions.
No external libraries, no YAML confusion, no broken JSON schemas.
4.3 The Data Structure
It all starts with something that’s both clear and expandable:
type Expense struct {
Date string // esempio: "2025-10-12"
Category string // esempio: "Food", "Transport"
Amount float64 // esempio: 12.50
Note string // esempio: "Lunch with friend"
}
Minimal, yet self-explanatory.
This is where the “Zero to WoW” mindset begins — a few lines that already feel intentional.
4.4 Version 1 – Full Save (overwrite)
The simplest reliable approach: write all expenses to the file each time you save.
import (
"encoding/csv"
"fmt"
"os"
"strconv"
)
func SaveExpenses(filename string, expenses []Expense) error {
file, err := os.Create(filename)
if err != nil {
return fmt.Errorf("cannot create file: %v", err)
}
defer file.Close()
writer := csv.NewWriter(file)
defer writer.Flush()
// Scriviamo l’intestazione per chiarezza
writer.Write([]string{"Date", "Category", "Amount", "Note"})
for _, e := range expenses {
record := []string{
e.Date,
e.Category,
strconv.FormatFloat(e.Amount, 'f', 2, 64),
e.Note,
}
if err := writer.Write(record); err != nil {
return fmt.Errorf("cannot write record: %v", err)
}
}
fmt.Printf("💾 %d expenses saved to %s\n", len(expenses), filename)
return nil
}
Output:
💾 5 expenses saved to expenses.csv
Generated File (expenses.csv):
Date,Category,Amount,Note
2025-10-10,Food,12.50,Lunch with friend
2025-10-10,Transport,3.00,Bus ticket
2025-10-11,Groceries,45.80,Weekly shopping
2025-10-11,Health,15.00,Pharmacy
2025-10-12,Entertainment,9.99,Streaming service
Readable. Portable. Reliable.
The kind of simplicity that quietly earns trust.
4.5 Version 2 – Incremental Save (append)
Sometimes, we just want to add a new expense without rewriting the entire file.
With Go, that’s a one-line change.
func AppendExpense(filename string, e Expense) error {
// Se il file non esiste, lo creiamo
fileExists := true
if _, err := os.Stat(filename); os.IsNotExist(err) {
fileExists = false
}
file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("cannot open file: %v", err)
}
defer file.Close()
writer := csv.NewWriter(file)
defer writer.Flush()
// Se è un nuovo file, scriviamo anche l’intestazione
if !fileExists {
writer.Write([]string{"Date", "Category", "Amount", "Note"})
}
record := []string{
e.Date,
e.Category,
strconv.FormatFloat(e.Amount, 'f', 2, 64),
e.Note,
}
if err := writer.Write(record); err != nil {
return fmt.Errorf("cannot write record: %v", err)
}
fmt.Printf("➕ Added expense: %s (%s $%.2f)\n", e.Note, e.Category, e.Amount)
return nil
}
Output:
➕ Added expense: Coffee (Food $2.50)
In Go, simplicity isn’t a limitation — it’s a survival skill.
Each function is small, pure, and easy to reason about.
4.6 Version 3 – Automatic Backup (because data deserves respect)
Before saving, let’s add a safety net: a quick backup of the existing file.
import "io"
func BackupFile(original string) error {
backup := original + ".bak"
in, err := os.Open(original)
if err != nil {
if os.IsNotExist(err) {
return nil // Nessun file da copiare
}
return err
}
defer in.Close()
out, err := os.Create(backup)
if err != nil {
return err
}
defer out.Close()
if _, err := io.Copy(out, in); err != nil {
return err
}
fmt.Println("🧩 Backup created:", backup)
return nil
}
Then use it like this:
BackupFile("expenses.csv")
SaveExpenses("expenses.csv", expenses)
Output:
🧩 Backup created: expenses.csv.bak
💾 6 expenses saved to expenses.csv
A single extra function turns a “toy project” into something you’d actually trust.
4.7 Version 4 – Loading Data Back
Saving is only half of persistence.
Now we need to read everything back — exactly as it was.
func LoadExpenses(filename string) ([]Expense, error) {
file, err := os.Open(filename)
if err != nil {
if os.IsNotExist(err) {
fmt.Println("🆕 No existing data found — starting fresh.")
return []Expense{}, nil
}
return nil, fmt.Errorf("cannot open file: %v", err)
}
defer file.Close()
reader := csv.NewReader(file)
records, err := reader.ReadAll()
if err != nil {
return nil, fmt.Errorf("cannot read file: %v", err)
}
var expenses []Expense
for i, record := range records {
if i == 0 { continue } // skip header
amount, _ := strconv.ParseFloat(record[2], 64)
expenses = append(expenses, Expense{
Date: record[0],
Category: record[1],
Amount: amount,
Note: record[3],
})
}
fmt.Printf("📂 Loaded %d expenses from file.\n", len(expenses))
return expenses, nil
}
Output:
📂 Loaded 17 expenses from file.
4.8 Version 5 – JSON Option (interoperable and elegant)
CSV is universal, but sometimes JSON makes integration easier — for example, syncing with a web service later.
Go makes the switch effortless.
import (
"encoding/json"
)
func SaveExpensesJSON(filename string, expenses []Expense) error {
data, err := json.MarshalIndent(expenses, "", " ")
if err != nil {
return err
}
return os.WriteFile(filename, data, 0644)
}
func LoadExpensesJSON(filename string) ([]Expense, error) {
data, err := os.ReadFile(filename)
if err != nil {
if os.IsNotExist(err) {
return []Expense{}, nil
}
return nil, err
}
var expenses []Expense
if err := json.Unmarshal(data, &expenses); err != nil {
return nil, err
}
fmt.Printf("📘 JSON loaded: %d records.\n", len(expenses))
return expenses, nil
}
Example expenses.Jason
[
{
"Date": "2025-10-10",
"Category": "Food",
"Amount": 12.50,
"Note": "Lunch with friend"
},
{
"Date": "2025-10-12",
"Category": "Health",
"Amount": 15.00,
"Note": "Pharmacy"
}
]
Readable. Shareable. Extendable.
4.9 The “WoW” Moment
Nothing flashy — just a small program that now remembers.
It quietly earns trust through reliability.
The wow doesn’t come from fancy frameworks or cloud APIs,
but from the feeling that your data is safe, your tool is honest, and your code makes sense.
Elegance isn’t about adding things — it’s about keeping only what’s essential.
This is the heart of From Zero to WoW:
real tools, built thoughtfully, that make life just a bit better.
