Code

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 saveItems and loadItems into their own storage.go file — you already have them, identical. Splitting a growing program across files is the capstone's job.