Refactor main.go — data survives a restart
The third milestone: your list survives a restart. Everything from lesson 9 stays — we wrap saving and loading around it, and give the fields names on disk.
1. Name the fields on disk — JSON tags:
type Item struct {
Title string `json:"title"`
Pages int `json:"pages"`
PagesRead int `json:"pagesRead"`
Read bool `json:"read"`
Category string `json:"category"`
}
2. Two new functions — save and load:
func saveItems(items []Item) error {
data, err := json.MarshalIndent(items, "", " ")
if err != nil {
return err
}
return os.WriteFile(saveFile, data, 0o644)
}
func loadItems() ([]Item, error) {
data, err := os.ReadFile(saveFile)
if errors.Is(err, fs.ErrNotExist) {
return []Item{}, nil // first run — no file yet, not an error
}
if err != nil {
return nil, err
}
var items []Item
if err := json.Unmarshal(data, &items); err != nil {
return nil, err
}
return items, nil
}
The key line is errors.Is(err, fs.ErrNotExist): a missing file is a normal first run, not a failure — it picks that one case out of every other possible error.
3. Load at startup, save on quit. main opens with items, err := loadItems() (and starts empty if it fails, without crashing); the q branch calls saveItems before "Saved. Bye!":
items, err := loadItems()
if err != nil {
fmt.Println("could not load saved items:", err)
items = []Item{}
}
case "q":
if err := saveItems(items); err != nil {
fmt.Println("save failed:", err)
}
fmt.Println("Saved. Bye!")
return
(Add "encoding/json", "errors", "io/fs" to the imports, and const saveFile = "items.json".)
Verify — add a book, quit, then run again and list:
# Run 1: add Dune / 412 / sci-fi, then q
Added: Dune
Saved. Bye!
# Run 2 (fresh start — the list is loaded from disk):
> 2
1. Dune 0/412 [reading]
The data survived the restart — open items.json and you'll see your shelf as readable text.
Tip. The capstone (lesson 14) moves
saveItemsandloadItemsinto their ownstorage.gofile — you already have them, identical. Splitting a growing program across files is the capstone's job.