Theory

The whole shelf in one go — JSON to a file

How do you turn a []Item into file content? We could write it line by line, but there is something better: JSON — a text format understood by Go, by humans, and by nearly every other language.

The encoding/json package turns the whole shelf into text with one call:

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

And at the end of a struct field you can add a tag — it sets the key name in the file:

type Item struct {
    Title string `json:"title"`
    Pages int    `json:"pages"`
}

The file will look like this — open it and read, it is just text:

[
  {
    "title": "Dune",
    "pages": 412
  }
]

For writing and reading we use the high-level pair — it opens AND closes the file itself (step 1's "open–use–close" handled for you):

err := os.WriteFile("items.json", data, 0o644)   // writes the whole file
data, err := os.ReadFile("items.json")           // reads the whole file

One design detail remains — the most important idea of this lesson: on the very first run the file does NOT exist yet. That is not an error, it is a normal situation, and it gets its own branch:

data, err := os.ReadFile(saveFile)
if errors.Is(err, fs.ErrNotExist) {
    return []Item{}, nil // first run — just an empty shelf
}

errors.Is separates "file missing" (normal → empty list) from every other trouble (a real error → return err). An app that crashes on its first run because it "could not find its own file" is an app nobody runs a second time.

Tip. This saveItems + loadItems design is not just an exercise: it is exactly the storage layer you will use in the final project. Build it well now — in the finale you simply bring it along.