Day 14 — Fuzzing, Golden Files & Go 1.26 Quality

Updated

July 30, 2025

Day 14 — Fuzzing, Golden Files & Go 1.26 Quality

Day 34 — Fuzzing

Stage IV · ~3h (theory-heavy)
Goal: Use Go’s built-in fuzzing (go test -fuzz) to stress parsers and codecs, seed corpora wisely, and turn crashers into regression tests.

Note

Fuzzing finds inputs you didn’t think of. It complements—not replaces—table-driven tests (Day 16).

Why this day exists

Table tests encode human imagination. Fuzzers encode mutation search:

  • Parsers (strconv, custom config, URL path rules)
  • Decoders (JSON subsets, binary headers)
  • Any function []byte → T or string → T with invariants

If you ship parsers without fuzzing, production becomes your fuzzer—with worse UX.


Theory 1 — Fuzz targets in Go

func FuzzParse(f *testing.F) {
    // seed corpus
    f.Add("42")
    f.Add("-1")
    f.Add("")

    f.Fuzz(func(t *testing.T, in string) {
        _, _ = Parse(in) // must not panic
    })
}
Piece Role
FuzzXxx Entry; name starts with Fuzz
f.Add(...) Seed interesting inputs
f.Fuzz(fn) fn args after *testing.T are fuzzed params
Supported param types e.g. []byte, string, integrals, bool, …

Run:

go test -fuzz=FuzzParse -fuzztime=30s

Without -fuzz, Fuzz tests still execute seed corpus like tests (good for CI smoke).


Theory 2 — What to assert

Fuzz targets should check invariants, not full equality to an oracle you don’t have.

Good invariants:

  • No panic
  • Parse error ⇒ do not use value
  • Parse success ⇒ Format(Parse(x)) round-trip (when defined)
  • Output length bounds
  • errors.Is categories
f.Fuzz(func(t *testing.T, in string) {
    v, err := Parse(in)
    if err != nil {
        return
    }
    out, err := Format(v)
    if err != nil {
        t.Fatalf("format: %v", err)
    }
    v2, err := Parse(out)
    if err != nil || v2 != v {
        t.Fatalf("roundtrip %q → %q → %v", in, out, v2)
    }
})

Theory 3 — Corpus management

On failure, Go writes a file under:

testdata/fuzz/FuzzParse/<hash>

Commit these files—they become regression seeds.

go test                       # runs seeds
go test -fuzz=FuzzParse -fuzztime=1x  # limited

Never discard crashers without minimizing understanding.


Theory 4 — Structured fuzzing with multiple args

f.Add(uint8(1), []byte("ab"))
f.Fuzz(func(t *testing.T, n uint8, b []byte) {
    // ...
})

Engine mutates each argument. Keep domains meaningful—or clamp inside:

if len(b) > 1<<20 {
    t.Skip()
}

Prefer fixing parser bounds over huge skips.


Theory 5 — Fuzzing vs race detector

You can combine:

go test -race -fuzz=FuzzFoo -fuzztime=20s

Slower but valuable for concurrent parsers. Day 36 deepens concurrent testing; today keep most fuzz targets single-goroutine pure functions.


Theory 6 — When fuzzing won’t help

  • UI layout
  • Business workflows needing external systems
  • Functions without interesting input domains
  • Non-deterministic time/random without injection

Fuzz pure, deterministic cores first.


Worked example — integer list parser

package parse

import (
    "fmt"
    "strconv"
    "strings"
)

// ParseList parses "1,2,3" into ints; empty string → empty slice.
func ParseList(s string) ([]int, error) {
    if s == "" {
        return nil, nil
    }
    parts := strings.Split(s, ",")
    out := make([]int, 0, len(parts))
    for _, p := range parts {
        p = strings.TrimSpace(p)
        if p == "" {
            return nil, fmt.Errorf("empty element")
        }
        n, err := strconv.Atoi(p)
        if err != nil {
            return nil, err
        }
        out = append(out, n)
    }
    return out, nil
}

func FormatList(xs []int) string {
    parts := make([]string, len(xs))
    for i, n := range xs {
        parts[i] = strconv.Itoa(n)
    }
    return strings.Join(parts, ",")
}

Fuzz:

func FuzzParseList(f *testing.F) {
    f.Add("")
    f.Add("1")
    f.Add("1,2,3")
    f.Add(" 1 , 2 ")
    f.Fuzz(func(t *testing.T, s string) {
        xs, err := ParseList(s)
        if err != nil {
            return
        }
        s2 := FormatList(xs)
        ys, err := ParseList(s2)
        if err != nil {
            t.Fatal(err)
        }
        if len(xs) != len(ys) {
            t.Fatalf("%v vs %v", xs, ys)
        }
        for i := range xs {
            if xs[i] != ys[i] {
                t.Fatalf("%v vs %v", xs, ys)
            }
        }
    })
}

Note: round-trip may fail if you allow spaces in input but format without spaces—that’s a spec decision. Fix Format/Parse or loosen invariant.


Lab 1 — Seed and run

Workspace: ~/lab/90daysofx/01-go/day34

mkdir -p ~/lab/90daysofx/01-go/day34 && cd ~/lab/90daysofx/01-go/day34
go mod init example.com/day34

Implement a small parser (CSV ints, key=value config, or subset JSON). Add Fuzz with ≥4 seeds.

go test
go test -fuzz=FuzzParse -fuzztime=30s

Lab 2 — Plant a bug, catch it

Introduce a panic on a rare input (e.g. single "%" character). Run fuzz until it fails. Commit the crasher corpus file. Fix the bug; ensure seeds still pass.


Lab 3 — Invariant design

Write three invariants for your parser. Rank them by strength. Implement the strongest that is true.


Lab 4 — CI-friendly fuzz smoke

Document in README:

# CI
go test ./...

# Nightly / local deep
go test -fuzz=FuzzParse -fuzztime=5m

Optional: -fuzzminimizetime awareness when minimizing crashers.


Common gotchas

Gotcha Fix
Non-deterministic fuzz body Inject clocks/rng
Huge allocations hanging host Bound input size
Asserting exact error strings Assert classes/invariants
Not committing testdata/fuzz Commit crashers
Fuzzing through network Fuzz pure decode only

Checkpoint

  • Wrote a Fuzz target with seeds
  • Ran -fuzz for ≥30s
  • Fixed at least one issue (planted or real)
  • Corpus under testdata/fuzz understood
  • Round-trip or no-panic invariant justified
  • go test passes seeds in non-fuzz mode

Commit

git add .
git commit -m "day34: fuzzing — parser Fuzz target + corpus"

Write three personal gotchas before continuing.


Tomorrow

Day 35 — Golden files & t.Cleanup: snapshot testing for encodings and deterministic artifacts with safe cleanup.


Day 35 — Golden files & Cleanup

Stage IV · ~3h (theory-heavy)
Goal: Use golden-file (snapshot) tests for encodings and rendered output; manage temp files with t.TempDir and t.Cleanup; know when goldens help vs harm.

Note

Golden tests freeze observable output. They excel at codecs, CLIs, and formatters—and rot when output is noisy or non-deterministic.

Why this day exists

Some correctness is “matches the fixture”:

  • Pretty-printed JSON
  • Serialized config
  • CLI help/output slices
  • Canonical error messages for stable APIs

Hand-asserting large strings is painful. Goldens store expected bytes on disk.


Theory 1 — Golden file pattern

testdata/
  foo.input.json
  foo.golden.json
func TestEncode(t *testing.T) {
    in, err := os.ReadFile("testdata/foo.input.json")
    if err != nil {
        t.Fatal(err)
    }
    var v Value
    if err := json.Unmarshal(in, &v); err != nil {
        t.Fatal(err)
    }
    got, err := EncodePretty(v)
    if err != nil {
        t.Fatal(err)
    }
    wantPath := "testdata/foo.golden.json"
    if *update {
        if err := os.WriteFile(wantPath, got, 0o644); err != nil {
            t.Fatal(err)
        }
    }
    want, err := os.ReadFile(wantPath)
    if err != nil {
        t.Fatal(err)
    }
    if !bytes.Equal(got, want) {
        t.Fatalf("mismatch\n got: %s\nwant: %s", got, want)
    }
}

Update flag

var update = flag.Bool("update", false, "update golden files")
go test -update ./...
# note: flag must be defined in TestMain or via flag.Parse in init carefully

Common pattern:

func TestMain(m *testing.M) {
    flag.Parse()
    os.Exit(m.Run())
}

Or use environment variable UPDATE_GOLDEN=1 to avoid flag plumbing conflicts.


Theory 2 — t.TempDir and t.Cleanup

dir := t.TempDir() // auto-removed after test
path := filepath.Join(dir, "out.txt")
f, err := os.CreateTemp("", "day35-*")
if err != nil {
    t.Fatal(err)
}
t.Cleanup(func() { os.Remove(f.Name()) })

Rules:

  • Prefer t.TempDir over manual deletes
  • t.Cleanup runs even on failure (LIFO)
  • Don’t rely on process exit for temp hygiene in long test binaries

Go 1.26 also adds t.ArtifactDir() for intentional retained outputs when -artifacts is set—use for CI-collected blobs; goldens stay in testdata/.


Theory 3 — Determinism requirements

Goldens fail if output includes:

  • Timestamps
  • Map iteration order (for text that ranges maps)
  • Random IDs
  • Absolute paths

Fixes:

  • Inject clock
  • Sort keys (json.Encoder.SetIndent + structured maps with sorted encode)
  • Normalize paths in comparison
  • Compare AST/semantic equality instead of bytes when needed

Theory 4 — Diff UX

On mismatch, print a readable diff:

import "github.com/google/go-cmp/cmp" // optional dep
// or simple line-oriented diff helper

For labs, a short helper is enough:

func diff(a, b string) string {
    // naive: show first mismatch index
}

Never dump megabytes without truncation in t.Fatalf.


Theory 5 — When not to golden

Prefer unit asserts Prefer golden
Numeric results Large structured text
Error sentinel types Exact formatted documents
Branch logic Regression on pretty printers

Goldens that update weekly become ignored noise—treat updates as reviews.


Theory 6 — Encoding lab target

Pick one:

  1. Canonical JSON for a config struct
  2. Custom line protocol encode/decode
  3. Markdown table renderer

Round-trip tests + golden for encode path.


Worked example — stable JSON golden

package conf

import (
    "bytes"
    "encoding/json"
)

type Config struct {
    Name  string   `json:"name"`
    Ports []int    `json:"ports"`
    Tags  []string `json:"tags"`
}

func Encode(c Config) ([]byte, error) {
    var buf bytes.Buffer
    enc := json.NewEncoder(&buf)
    enc.SetIndent("", "  ")
    if err := enc.Encode(c); err != nil {
        return nil, err
    }
    return buf.Bytes(), nil
}

Test uses testdata/config.golden.json. Note Encode adds trailing newline—golden must match.


Worked example — Cleanup with servers

func TestServer(t *testing.T) {
    // if using httptest:
    // srv := httptest.NewServer(...)
    // t.Cleanup(srv.Close)
}

Always cleanup listeners, temp files, and chdir restores:

wd, _ := os.Getwd()
t.Cleanup(func() { _ = os.Chdir(wd) })

Lab 1 — Golden encode

Workspace: ~/lab/90daysofx/01-go/day35

mkdir -p ~/lab/90daysofx/01-go/day35/testdata
cd ~/lab/90daysofx/01-go/day35
go mod init example.com/day35
  1. Define a struct + pretty encoder.
  2. Write golden file.
  3. Test equality.
  4. Change encoder; see failure; update golden deliberately; review diff.
go test ./...

Lab 2 — Update workflow

Implement -update flag or UPDATE_GOLDEN env. Document:

UPDATE_GOLDEN=1 go test ./...
git diff testdata/

Treat the diff as a PR review surface.


Lab 3 — TempDir pipeline

Write a test that:

  1. Creates input file in t.TempDir
  2. Runs a function writing output beside it
  3. Compares to golden content (not path)
  4. Leaves no residue outside temp
go test -race ./...

Lab 4 — Non-determinism drill

Intentionally include time.Now() in output; watch golden flake. Fix by injecting a func() time.Time clock. Re-stabilize.


Common gotchas

Gotcha Fix
CRLF vs LF Normalize or run on consistent OS; prefer \n
Map range order in custom encoders Sort keys
Updating goldens blindly Review every byte change
Storing secrets in testdata Never
Huge binary goldens Prefer text or semantic compares

Checkpoint

  • Golden test for an encoder
  • Update workflow documented
  • Uses t.TempDir / t.Cleanup correctly
  • Fixed a non-determinism issue
  • Understands 1.26 ArtifactDir vs goldens
  • go test green

Commit

git add .
git commit -m "day35: golden files + TempDir/Cleanup labs"

Write three personal gotchas before continuing.


Tomorrow

Day 36 — Concurrent tests / synctest: testing concurrent code without flakes—race detector, parallelism, and modern sync-test awareness.


Day 36 — Concurrent tests / synctest

Stage IV · ~3h (theory-heavy)
Goal: Test concurrent code without flakes: t.Parallel discipline, race detector in CI shape, timeout assertions, and synctest awareness for deterministic concurrency testing where available.

Note

Flaky concurrent tests train teams to ignore red CI. Your job is deterministic failure when code is wrong—and green when code is right.

Why this day exists

Concurrent tests go wrong by:

  • Sleeping “enough”
  • Depending on scheduler luck
  • Sharing global state across tests
  • Forgetting -race
  • Deadlocking the test binary

You already have primitives (Days 19–28). Today is the test harness craft.


Theory 1 — What good concurrent tests prove

Property How to test
Race-freedom go test -race
Cancel exits assert return before deadline
Concurrency bound atomic in-flight max
Correctness under load many goroutines + final assert
No leak (coarse) NumGoroutine settle / join API

Prefer observable postconditions over “didn’t crash once.”


Theory 2 — t.Parallel rules

func TestA(t *testing.T) {
    t.Parallel()
    // ...
}

Rules:

  1. No shared mutable globals without sync
  2. Package TestMain / suite setup must be parallel-safe
  3. Use subtests carefully—t.Parallel in subtests after t.Run scaffolding
  4. Parallel + time-sensitive sleeps ⇒ flakes

Pattern for subtests:

func TestAll(t *testing.T) {
    t.Parallel()
    cases := []string{"a", "b"}
    for _, tc := range cases {
        tc := tc
        t.Run(tc, func(t *testing.T) {
            t.Parallel()
            // use tc
        })
    }
}

Theory 3 — Never sleep for sync (almost)

Bad:

go doWork()
time.Sleep(100 * time.Millisecond)
assertDone()

Better:

done := make(chan struct{})
go func() {
    defer close(done)
    doWork()
}()
select {
case <-done:
case <-time.After(2 * time.Second):
    t.Fatal("timeout")
}

Or block on errgroup.Wait / API that joins.

Sleep is OK only as a scheduler settle upper bound after proper joins—not as the join itself.


Theory 4 — Testing cancel

func TestCancel(t *testing.T) {
    ctx, cancel := context.WithCancel(context.Background())
    started := make(chan struct{})
    errCh := make(chan error, 1)

    go func() {
        errCh <- engine.Run(ctx, slowItems(), func(ctx context.Context, it Item) error {
            close(started)
            <-ctx.Done()
            return ctx.Err()
        })
    }()

    <-started
    cancel()

    select {
    case err := <-errCh:
        if !errors.Is(err, context.Canceled) {
            t.Fatalf("got %v", err)
        }
    case <-time.After(2 * time.Second):
        t.Fatal("Run did not return")
    }
}

Theory 5 — Synctest awareness

Go’s testing ecosystem has been moving toward better deterministic concurrency testing (often discussed as synctest / bubble-style testing: control when goroutines advance, avoid real wall-clock flakiness).

What you should know on Go 1.26.x path

  1. Classical tools remain foundational: -race, channels, ctx, errgroup, testing timeouts (go test -timeout).
  2. Experimental / evolving helpers may allow running concurrent logic in a controlled scheduler bubble so tests don’t depend on real time.Sleep durations.
  3. For this volume: do not depend on unstable APIs as your only skill—learn the portable patterns above first.
  4. When your toolchain includes testing/synctest (or successor docs), practice one example from official docs and note version requirements in your journal.
// Pseudocode / version-gated sketch — verify against your Go 1.26.x docs:
//
// synctest.Run(func() {
//     // goroutines + fake time advancement
// })

If the package is not available in your exact patch level, complete labs with classical techniques and write a short note: “synctest: checked docs on <date>.”


Theory 6 — go test flags for concurrency work

go test -race -count=20 ./...
go test -timeout 30s ./...
go test -parallel 4 ./...
go test -run TestCancel -v

CI snippet:

go test -race -count=1 -timeout 5m ./...

-count=1 disables cache; critical for flake hunting.


Theory 7 — Deadlocking tests

If a test hangs:

  1. go test -timeout 5s to fail fast
  2. Check missing Done / channel receive
  3. Avoid waiting on locks held by the test goroutine

For interactive debug, SIGQUIT (where supported) dumps stacks.


Worked example — bound test

func TestWorkerLimit(t *testing.T) {
    var (
        current atomic.Int64
        max     atomic.Int64
    )
    items := make([]int, 50)
    _, err := MapLimit(context.Background(), items, 5, func(ctx context.Context, v int) (int, error) {
        c := current.Add(1)
        for {
            m := max.Load()
            if c <= m || max.CompareAndSwap(m, c) {
                break
            }
        }
        time.Sleep(5 * time.Millisecond)
        current.Add(-1)
        return v, nil
    })
    if err != nil {
        t.Fatal(err)
    }
    if max.Load() > 5 {
        t.Fatalf("max in-flight %d", max.Load())
    }
}

Lab 1 — Flake then fix

Workspace: ~/lab/90daysofx/01-go/day36

  1. Write a deliberately flaky test using Sleep.
  2. Run -count=50 to see intermittent failures (or force failure).
  3. Rewrite with channel/join.
  4. Prove green at -count=50.
go test -race -count=50 ./...

Lab 2 — Parallel suite hygiene

Create two tests that would race on a package-level map if both parallel. Show failure with -race, then isolate state (or protect) so t.Parallel is safe.


Lab 3 — Cancel + timeout tests

Port Day 29/30 engine tests: cancel and deadline paths with 2s upper bound. No sleeps longer than needed inside assertions.


Lab 4 — Synctest spike

  1. Run go doc testing / search release notes for synctest on your 1.26.x.
  2. If present, implement one official-style example.
  3. If absent, document classical substitutes and keep labs green.

Common gotchas

Gotcha Fix
Global test counters Per-test state
t.Parallel + shared files TempDir per test
Infinite Wait on bug -timeout
Ignoring race in parallel tests Always -race for concurrent packages
Over-synchronizing production code for tests Prefer injectable seams

Checkpoint

  • Eliminated a sleep-based flake
  • Safe t.Parallel usage demonstrated
  • Cancel test fails if cancel ignored
  • -race -count=20+ clean
  • Synctest researched for 1.26.x
  • Knows -timeout and -count roles

Commit

git add .
git commit -m "day36: concurrent tests — de-flake, parallel, cancel asserts"

Write three personal gotchas before continuing.


Tomorrow

Day 37 — Go 1.26 quality pass: Green Tea GC default, new(expr), rewritten go fix, and a deliberate modernization sweep.


Day 37 — Go 1.26 quality pass

Stage IV · ~3h (theory-heavy + hygiene)
Goal: Apply a deliberate Go 1.26.x quality pass: understand Green Tea GC as default, use new(expr), run the modernized go fix, and leave Stage IV code cleaner and more idiomatic.

Note

This day is about toolchain literacy, not chasing every experimental GOEXPERIMENT. Prefer stable defaults; opt into experiments only with notes.

Why this day exists

Go releases quietly change:

  • Runtime performance (GC)
  • Language ergonomics (new)
  • Tooling (go fix modernizers)
  • Testing/runtime diagnostics

If you never re-read release notes, you write 2019-style code on a 2026 compiler. Today you align your labs with 1.26.


Theory 1 — Confirm your toolchain

go version
# expect go1.26.x

go env GOVERSION GOTOOLCHAIN GOROOT

Module line:

// go.mod
go 1.26

If go mod init on 1.26 created go 1.25.0 (policy: default to N-1), raise deliberately:

go get go@1.26.0
# or edit go.mod go line and go mod tidy

Know the policy: new modules may target the previous minor for broader compatibility—this volume still teaches 1.26 features.


Theory 2 — Green Tea GC is default

From Go 1.26 release notes:

  • Green Tea garbage collector (experimental in 1.25) is enabled by default
  • Aims for better locality/scalability marking small objects
  • Expected ~10–40% reduction in GC overhead in GC-heavy real programs (workload-dependent)
  • Additional gains possible on newer amd64 with vectorized scanning
  • Opt out (temporary): build with GOEXPERIMENT=nogreenteagc
  • Opt-out expected to go away in a future release (notes mention 1.27 direction)

What you should do as an app developer

  1. Don’t micro-tune GC before measuring (Stage VII covers pprof).
  2. Do re-run critical latency benches when upgrading 1.25 → 1.26.
  3. If you suspect GC issues after upgrade, compare with GOEXPERIMENT=nogreenteagc only as a diagnostic, then file issues if truly regressing—don’t permanently cargo-cult the opt-out.
# diagnostic A/B (same benchmark)
go test -bench=BenchmarkFoo -benchmem ./...
GOEXPERIMENT=nogreenteagc go test -bench=BenchmarkFoo -benchmem ./...

Theory 3 — Language: new(expr)

Historically:

p := new(int)
*p = 42

Go 1.26 allows an expression operand:

p := new(42)          // *int pointing to 42
q := new(yearsSince(born)) // allocate + initialize from call result

If expr has type T, new(expr) has type *T and the pointed-to value is initialized to expr.

Why it matters

Optional JSON/protobuf fields often need pointers:

type Person struct {
    Name string `json:"name"`
    Age  *int   `json:"age"`
}

age := 40
p := Person{Name: "Ann", Age: &age}

// 1.26:
p = Person{Name: "Ann", Age: new(40)}
// or new(yearsSince(born))

Cleaner than temporary variables for one-off pointers.

Not a replacement for everything

  • Prefer values when nil-ability isn’t required
  • Don’t invent pointer forests for vanity

Theory 4 — Self-referential type parameters (recap)

Also in 1.26 language notes (Day 32):

type Adder[A Adder[A]] interface {
    Add(A) A
}

Quality pass: if you wrote awkward workarounds, simplify constraints where self-ref is natural.


Theory 5 — Rewritten go fix (modernizers)

In Go 1.26, go fix is revamped:

  • Built on the same analysis framework as go vet
  • Home of modernizers—push-button updates toward modern idioms/APIs
  • Old historical fixers removed
  • Includes suite of language/library modernizations + source-level inlining via //go:fix inline (advanced)
go fix ./...
go vet ./...
go test -race ./...

Discipline

  1. Clean git status before go fix
  2. Run fix
  3. Review diffs like a PR
  4. Run tests
  5. Commit separately: chore: go fix modernizers

Do not blind-apply across huge monorepos without review.


Theory 6 — Testing quality on 1.26

Awareness items from release notes:

  • t.ArtifactDir / -artifacts for retained test outputs
  • B.Loop inlining improvements for benchmarks
  • Prefer modern test helpers you already use (t.TempDir, t.Cleanup)

Optional experiment: goroutine leak profiles in tests for Stage III engines.


Theory 7 — Quality checklist (Stage IV modules)

Check Command / action
Version go version → 1.26.x
Modules tidy go mod tidy
Vet go vet ./...
Fix go fix ./... + review
Test race go test -race ./...
Fuzz smoke go test (seeds)
Formatting gofmt -w . / go fmt ./...
Static hints optional staticcheck (Stage VII deep)

Worked example — new(expr) in tests

func TestPersonJSON(t *testing.T) {
    p := Person{Name: "Ada", Age: new(36)}
    b, err := json.Marshal(p)
    if err != nil {
        t.Fatal(err)
    }
    if !bytes.Contains(b, []byte(`"age":36`)) {
        t.Fatalf("%s", b)
    }
}

Worked example — GC note in README

## Runtime

Built with Go 1.26+. Green Tea GC is the default; no GODEBUG required.
If diagnosing GC regressions vs 1.25, compare GOEXPERIMENT=nogreenteagc builds.

Lab 1 — Inventory & upgrade

Workspace: pick day31day36 modules or a combined Stage IV folder.

go version
# ensure go.mod go 1.26 (or document N-1 policy choice)
go mod tidy

Journal any mismatch between local toolchain and module directive.


Lab 2 — Apply new(expr)

Find ≥2 places using temp vars only to take addresses for optional fields. Rewrite with new(expr) where clearer. Tests remain green.


Lab 3 — go fix pass

git status  # clean
go fix ./...
git diff
go test -race ./...

Write 5 bullet notes on what changed (or “no changes—already modern”).


Lab 4 — GC / runtime awareness lab

  1. Write a small allocation-heavy benchmark (e.g. many small struct allocs).
  2. Run with default GC.
  3. Run with GOEXPERIMENT=nogreenteagc.
  4. Record ns/op and note N=1 machine anecdote ≠ truth—learn the method.
go test -bench=BenchmarkAlloc -benchmem
GOEXPERIMENT=nogreenteagc go test -bench=BenchmarkAlloc -benchmem

Optional: enable leak profile experiment on a known-leaky snippet from Day 25.


Common gotchas

Gotcha Fix
Blind go fix on dirty tree Commit first
Pinning nogreenteagc forever Diagnostic only
Raising go version without CI Align CI Quarto/book env notes separately; app modules use 1.26
Overusing pointers via new Values by default
Skipping tests after fix Always re-test

Checkpoint

  • Running Go 1.26.x locally
  • Explains Green Tea GC default + opt-out diagnostic
  • Used new(expr) idiomatically
  • Ran go fix and reviewed
  • go test -race green after pass
  • Recorded bench A/B method (even if noisy)

Commit

git add .
git commit -m "day37: Go 1.26 quality — new(expr), go fix, GC notes"

Write three personal gotchas before continuing.


Tomorrow

Day 38 — Stage IV gate: generic helper + deep tests (fuzz and/or goldens + concurrent tests) on a real package—prove Stage IV complete.