Skip to content

Day 031 — os, Files & Exit Codes

Month 2 · Week 1 · ⬅ Day 030 · Day 032 ➡ · Journal index

🎯 Learning Objective

Open, read, write, and close files with the os package, handle the (*os.File, error) idiom, and control process exit codes and arguments the idiomatic way.

📚 Topics

  • os.Open / os.Create / os.OpenFile, defer f.Close()
  • os.ReadFile / os.WriteFile for whole-file convenience
  • os.Stdin / os.Stdout / os.Stderr, os.Args, os.Getenv
  • os.Exit and the defer-doesn't-run trap

📖 Reading / Sources

📝 Notes

  • os.Open(name) opens read-only and returns (*os.File, error); os.Create(name) opens write-only, truncating/creating with mode 0666. For full control use os.OpenFile(name, flag, perm) with flags like os.O_RDWR|os.O_APPEND|os.O_CREATE → [[os-file]].
  • An *os.File is both an io.Reader and io.Writer, so everything from Day 029–030 (io.Copy, bufio.Scanner) works directly on files.
  • Always defer f.Close() right after a successful open — but only after checking the error, because f is nil on failure. For write files, a failing Close can mean lost data, so on critical writes check the Close error explicitly rather than only deferring.
  • os.ReadFile(name) returns the whole file as []byte; os.WriteFile(name, data, perm) writes it in one call. These replaced the old ioutil versions and are perfect for small files.
  • The three standard streams are package vars: os.Stdin, os.Stdout, os.Stderr — all *os.File. Write diagnostics to os.Stderr so they don't pollute piped stdout.
  • os.Args is []string where os.Args[0] is the program name; real arg parsing belongs to the flag package (later this month).
  • Exit codes: os.Exit(code) ends the process immediately with that status. 0 = success; non-zero = failure. os.Exit does NOT run deferred functions — flush/close before calling it, or prefer returning an error up to main → [[os-exit-trap]].
  • Idiom: keep main thin — do work in a run() error function, then if err := run(); err != nil { fmt.Fprintln(os.Stderr, err); os.Exit(1) }. This lets defers run inside run before the exit.

💻 Code Examples

This day's code has side effects (writing files, calling os.Exit), so it stays here as a snippet rather than a runnable example:

func main() {
    if err := run(); err != nil {
        fmt.Fprintln(os.Stderr, "error:", err)
        os.Exit(1) // non-zero status; defers in run() already ran
    }
}

func run() error {
    f, err := os.OpenFile("out.txt", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o644)
    if err != nil {
        return fmt.Errorf("open: %w", err)
    }
    defer f.Close() // runs because we return, not os.Exit, from here

    if _, err := fmt.Fprintln(f, "appended line"); err != nil {
        return fmt.Errorf("write: %w", err)
    }
    // Whole-file convenience:
    data, err := os.ReadFile("out.txt")
    if err != nil {
        return fmt.Errorf("read: %w", err)
    }
    fmt.Printf("file now has %d bytes\n", len(data))
    return nil
}

🏋️ Exercises / Practice

Exercise Status Link
(Reuse) linestats works on os.Open'd files via io.Reader exercises/month-02/week-1/linestats

🐛 Mistakes Made

  • Called os.Exit(1) from deep in the code and was surprised my defer f.Close() never ran — moved exits to main.
  • Deferred f.Close() before checking the os.Open error → would have called Close on a nil file on the error path.

❓ Open Questions

  • When should I use os.OpenFile with explicit flags vs. the os.Create/os.ReadFile conveniences?

🧠 Active Recall (answer without looking)

  1. Q: Why can os.Exit(1) cause data loss in a program using bufio.Writer?

    A `os.Exit` terminates immediately and runs **no** deferred functions, so a `defer w.Flush()` never executes and the buffered bytes are lost. Flush before exiting, or return an error to `main`.

  2. Q: What's the value and type of os.Args[0]?

    A It's a `string` — the program/command name as invoked. Actual arguments start at `os.Args[1]`.

🪶 Feynman Reflection

A file handle is just a labeled pipe the OS hands you; an *os.File is both a faucet (Reader) and a drain (Writer), so all my stream tools work on it. The one landmine is os.Exit: it's an emergency stop that skips all the cleanup defer promised, so I keep exits at the very top of the program where there's nothing left to clean up.

🕳️ Knowledge Gaps

  • File permission bits and umask interaction; os.Stat/os.FileInfo details.

✅ Summary

I can open/read/write/close files idiomatically, route output to the right stream, read args/env, and control exit codes without losing buffered data.

⏭️ Next Steps / Prep for Tomorrow

  • Day 032: fmt verbs deep dive — formatting everything precisely.

Time spent Difficulty Confidence
90 min 🟦🟦⬜⬜⬜ 🟦🟦🟦⬜⬜

Suggested commit: docs(journal): os, files, and exit codes (day 031)