Theory

An error is a value, not an explosion

Files are the first place your program meets the outside world. And the outside world is unreliable: the file may not exist, the disk may be full, permissions may say no. So Go applies everywhere the rule you already know from strconv.Atoi:

data, err := os.ReadFile("items.json")
if err != nil {
    // something failed — we REACT here
}

In Go an error is not an explosion — it is a plain returned value. A function does not crash your program; it calmly says "that failed, here is why" and leaves the decision to you. Every file operation returns an error, and you must check every one.

At the lowest level a file lives in three steps: open → use → close. An open file is a held resource, which is why Go has defer — "do this when the function ends, no matter what happens":

f, err := os.Create("items.json")
if err != nil {
    return err
}
defer f.Close() // the close is scheduled IMMEDIATELY after opening

Gotcha. Two sins that "work" on your machine and break the program on everyone else's:

  1. Ignoring the error: data, _ := os.ReadFile(...) — the file is missing, data is empty, and the program keeps pretending everything is fine, then breaks later, far from the real cause. Check the error where it is born.
  2. Forgetting defer f.Close(): an unclosed file — unwritten data can sit in a buffer (the file ends up empty or half-written), and the OS keeps the file locked. The habit: wrote os.Create or os.Open — the next line after the error check is defer f.Close().

Good news: in the next step you will see that for everyday work Go gives you a pair of functions that handle the opening and closing for you — but checking the error stays YOUR job, always.