Day 18 — Stage II gate
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.
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 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 validateKeep main thin
If CLI exists, it only maps flags → library → exit codes.
Theory 4 — API design checklist
- Minimal exports — unexport helpers
- Errors are part of API — document sentinels
- Accept interfaces, return concrete where it helps
- Context first if any I/O or cancellation
- No global mutable state
- 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.Isorerrors.As
- Fresh subjects per subtest (no leaked state)
go test ./...clean
Stretch:
- Fake behind interface with service test
-coverglance; 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
- Can a stranger use the package from README alone?
- Are failure modes tested?
- Any panic left for user input?
- Is
go.modfree of accidentalreplace?
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/day18Implement 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.