Day 18 — Stage II gate

Updated

July 30, 2026

Day 18 — Stage II gate

Stage II · ~3h (integration)
Goal: Ship a publishable-shaped Go module: clear public API, table-driven tests, at least one interface seam with a fake, idiomatic errors (Is/As/Join as needed), README, and go test ./... green on Go 1.26.

Important

Stage II gate is about library quality, not product breadth. A small well-tested package beats a sprawling untested app. CLI wrappers are optional glue.

Why this day exists

Stage II skills—interfaces, assertions, errors, modules, tests, fakes—only count when integrated:

  • Others can import your package
  • Tests document and protect behavior
  • Errors are inspectable
  • Layout matches module norms

Pass this gate before concurrency multiplies complexity.


Theory 1 — Definition of done

Requirement Evidence
Module path Sensible go.mod (example.com/day18/... fine for lab)
Public API Exported types/funcs with godoc comments
Tests go test ./... pass; tables for core logic
Seam Interface + fake or pure package easily tested without I/O
Errors Sentinels and/or typed errors; no panics for input
README Install/use/examples/exit or API notes
Toolchain Builds on Go 1.26

Explicit non-goals

  • No race-heavy concurrency requirement
  • No external cloud services
  • No mock codegen frameworks
  • No perfect semantic version publish to a real VCS host required (shape only)

Theory 2 — Project menu (pick one)

B — Domain service module

Extract Day 10/11 inventory or todo into:

module/
  go.mod
  README.md
  todo/           # public API
  todo/fake/      # or todo/mem.go fake store
  cmd/todo/       # optional

Service tests use fakes.

C — Parsing library

Parse a small language: KEY=VALUE env files, or log lines KIND payload.

  • Exported Parse
  • Typed errors for syntax
  • Golden-ish tables of inputs

D — Retry / backoff helper (stdlib only)

func Do(ctx context.Context, attempts int, fn func() error) error

Fake clock optional; tests inject failing fn closures.


Theory 3 — Suggested layout

day18/
  go.mod
  README.md
  <lib>/
    doc.go          # package comment
    api.go
    errors.go
    api_test.go
  cmd/<tool>/main.go   # optional

Package comment

// Package validate implements small composable validators with inspectable errors.
package validate

Keep main thin

If CLI exists, it only maps flags → library → exit codes.


Theory 4 — API design checklist

  1. Minimal exports — unexport helpers
  2. Errors are part of API — document sentinels
  3. Accept interfaces, return concrete where it helps
  4. Context first if any I/O or cancellation
  5. No global mutable state
  6. Examples in README that match tests
// ErrNotFound is returned when an item does not exist.
var ErrNotFound = errors.New("not found")

Theory 5 — Test bar for the gate

Minimum:

  • ≥ 1 table-driven test with ≥ 4 cases
  • ≥ 1 error-path assertion using errors.Is or errors.As
  • Fresh subjects per subtest (no leaked state)
  • go test ./... clean

Stretch:

  • Fake behind interface with service test
  • -cover glance; note gaps
  • testdata/ fixture

Theory 6 — README shape

# module name

What problem it solves (3–5 sentences).

## Install

go get example.com/your/module@latest   # or clone path for lab

## Example

Short Go snippet using the public API.

## Errors

Which sentinels / types callers should check.

## Develop

go test ./...

For lab modules not published, document go test and local replace/go work if used.


Worked examples bank

Example A — Public validation API sketch

package validate

import "errors"

var ErrInvalid = errors.New("invalid")

type Result struct {
    // optional structured outcome
}

func Email(s string) error { /* ... */ }

func Check(email, password string) error {
    return errors.Join(Email(email), Password(password))
}

Example B — Test excerpt

func TestCheck(t *testing.T) {
    tests := []struct {
        name, email, pass string
        wantFields        []string
    }{
        {"ok", "a@b.co", "password1", nil},
        {"both bad", "", "x", []string{"email", "password"}},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := Check(tt.email, tt.pass)
            // assert fields via helper walking Join
            _ = err
        })
    }
}

Example C — Exit mapping CLI

func main() {
    if err := run(os.Args[1:]); err != nil {
        fmt.Fprintln(os.Stderr, err)
        if errors.Is(err, validate.ErrInvalid) {
            os.Exit(1)
        }
        os.Exit(2)
    }
}

Example D — Self-review questions

  1. Can a stranger use the package from README alone?
  2. Are failure modes tested?
  3. Any panic left for user input?
  4. Is go.mod free of accidental replace?

Labs

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

Lab 1 — Build the gate module

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

Implement your chosen project to the definition of done.

Lab 2 — Test suite pass

go test ./...
go test -cover ./...

Fix until green. Add one case that failed during development (regression).

Lab 3 — README + example

Write README. If time allows, add ExampleCheck in _test.go for go test example output (optional func ExampleXxx()).

func ExampleEmail() {
    fmt.Println(Email("a@b.co") == nil)
    // Output:
    // true
}

Lab 4 — Gate retrospective

Write RETRO.md (10–15 lines):

  • What Stage II skill was hardest
  • What you would refactor before Stage III
  • One bug tests caught

Common gotchas

Gotcha Fix
Logic only in main Move to library package
Tests missing error paths Add Is/As cases
README lists features you did not ship Document reality
Left replace in go.mod Remove
Panic on bad input Return error
Interface without test double Fake or drop interface

Checkpoint

  • go test ./... green on 1.26
  • Public API documented
  • Table-driven tests present
  • Idiomatic errors
  • README usable
  • Retro written
  • Ready for goroutines without unfinished Stage II debt

Commit

git add .
git commit -m "day18: stage II gate module + tests"

Tomorrow

Stage III begins — Day 19 Goroutines: the concurrency model (G/M/P light), launching work safely, and why “just add go” is not a strategy—without channels yet until Day 20.