Day 10 — Stage I gate

Updated

July 30, 2026

Day 10 — Stage I gate

Stage I · ~3h (integration)
Goal: Ship a small, stdlib-only CLI that integrates the Stage I toolkit—modules, types, control flow, functions/defer, slices, maps, structs/methods, pointers where justified, and multi-package layout—with a README others can follow.

Important

This is a gate, not a feature festival. A polished narrow tool beats a half-broken platform. Cut scope ruthlessly.

Why this day exists

Stage I topics only stick when they cohere in one artifact. Gates force:

  • Design trade-offs under time
  • Error handling end-to-end
  • Package boundaries that tests can import
  • README as part of the deliverable

If Day 10 works, you are ready for interfaces, real error wrapping, and modules depth in Stage II—not still fighting slice headers.


Theory 1 — What “good enough” means here

Must-haves

Bar Evidence
Multi-package module cmd/... + at least one library package
Domain types Structs + methods (not only maps of primitives)
Collections Slices and/or maps used appropriately
Errors as values No panic for expected bad input
Process UX Non-zero exit on failure; usage on misuse
Docs README: purpose, build, examples, exit codes
Build go build ./... clean on Go 1.26

Explicit non-goals

  • No web framework
  • No external modules required (stdlib only)
  • No concurrency requirement
  • No database
  • No perfect CLI parser library (flag is enough)

Theory 2 — Project idea menu (pick one)

A — Todo list CLI

todo add "buy milk"
todo list
todo done 1
todo rm 1

Storage: JSON file in cwd or $HOME using encoding/json lightly (allowed—stdlib). Or line-oriented text if you want less encoding.

B — Inventory CLI (Day 7–9 upgrade)

inv add sku name qty price
inv take sku n
inv list
inv total

In-memory + optional save/load.

C — Log classifier (Day 3 upgrade)

classify <file>
classify # stdin

Structured summary + exit 1 on fatal; multi-package (classify/engine, cmd/classify).

D — Word index

Build inverted index: word → line numbers. Commands: index <file>, find <word>.

Pick one. Do not merge all four.


Theory 3 — Suggested layout

day10/
  go.mod
  README.md
  cmd/<app>/main.go
  <domain>/
    ...
  internal/
    ...

main stays thin

func main() {
    if err := run(os.Args[1:]); err != nil {
        fmt.Fprintf(os.Stderr, "%v\n", err)
        os.Exit(exitCode(err))
    }
}

Domain owns rules

Validation, aggregates, and pure logic live outside main so Stage II can put tests on them without refactoring everything.


Theory 4 — Exit code policy

Situation Code
Success 0
Domain / runtime error 1
Usage / args 2
var ErrUsage = errors.New("usage")

func exitCode(err error) int {
    if errors.Is(err, ErrUsage) {
        return 2
    }
    return 1
}

Theory 5 — README template

# <tool>

One paragraph: what it does.

## Requirements

- Go 1.26+

## Build

    go build -o <tool> ./cmd/<tool>

## Usage

Examples with expected output.

## Exit codes

| Code | Meaning |
|------|---------|
| 0 | OK |
| 1 | Failure |
| 2 | Usage |

## Design notes

2–5 bullets: packages, storage, limitations.

Worked examples bank

Example A — Flag + subcommand sketch

func run(args []string) error {
    if len(args) < 1 {
        return fmt.Errorf("%w: <command> [args]", ErrUsage)
    }
    switch args[0] {
    case "list":
        return cmdList()
    case "add":
        return cmdAdd(args[1:])
    default:
        return fmt.Errorf("%w: unknown command %q", ErrUsage, args[0])
    }
}

Example B — Persistence sketch (JSON)

func load(path string) (*Store, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        if os.IsNotExist(err) {
            return NewStore(), nil
        }
        return nil, err
    }
    var s Store
    if err := json.Unmarshal(data, &s); err != nil {
        return nil, err
    }
    s.ensure()
    return &s, nil
}

func save(path string, s *Store) error {
    data, err := json.MarshalIndent(s, "", "  ")
    if err != nil {
        return err
    }
    return os.WriteFile(path, data, 0o644)
}

Example C — Smoke script

#!/usr/bin/env bash
set -euo pipefail
go build -o app ./cmd/app
./app list >/dev/null
./app add "demo" 
./app list | grep -q demo
! ./app nope   # expect failure

Labs

Suggested workspace: ~/lab/90daysofx/go/day10

Lab 1 — Ship the gate project

mkdir -p ~/lab/90daysofx/go/day10
cd ~/lab/90daysofx/go/day10
go mod init example.com/day10

Implement your chosen tool to the must-have bar.

Lab 2 — Bad input matrix

Manually run at least:

  1. No arguments → usage, exit 2
  2. Unknown subcommand → exit 2
  3. Domain error (missing id, empty name, …) → exit 1 with clear message
  4. Happy path → exit 0

Record commands and outputs in README or SMOKE.md.

Lab 3 — Self-review checklist

Without adding features, answer:

  • Where could a package boundary improve tests?
  • Any panic paths for normal errors?
  • Any nil map writes?
  • Slice ownership documented if you return internal slices?
  • README sufficient for a teammate?

Lab 4 — Optional micro-test

One TestX in a library package that locks a pure function (parse, total, classify line). Stage II will expand testing culture.


Common gotchas

Gotcha Fix
Scope creep Cut to core commands
Logic trapped in main Extract packages mid-gate if needed
Ignoring exit codes Define and test them
README afterthought Write examples while building
Panic on bad input Return error
“I’ll refactor later” forever Gate requires shippable shape now

Checkpoint

  • Tool builds with Go 1.26
  • Multi-package layout
  • Structs + methods + maps/slices in play
  • Bad input → non-zero exit
  • README with build + examples
  • Can explain slice header and map comma-ok from this codebase
  • Ready to discuss interfaces without unfinished Stage I panic

Commit

git add .
git commit -m "day10: stage I gate mini CLI"

Tomorrow

Stage II begins — Day 11 Interfaces: implicit satisfaction, small interfaces, any, and refactoring domain behavior behind interfaces without mock frameworks.