agentleFS
h0rn3th0rn3t/golang-skills/skills/go-code/SKILL.md

go-code

Skill7 starsChanged 9 days agoApache-2.0View on GitHub

Use when writing or fixing Go code (a behavior-preserving refactor routes to go-code-refactor), whether or not the task has one obvious topic — it loads the go-* skills the task needs and runs the closing gate. Also use when asked to implement a Go package, function, or handler whose declarations and documentation already exist — write the bodies, fill in a stub, replace panic("not implemented") — even if the request names only the package. Use it too when /go-code modifies another workflow (e.g. /opsx:apply /go-code); as a modifier it selects Go rules, not a change name or path.

The file

We split this skill into the rules it gives an agent

9 rules. About 0 are common, 5 are distinctive, and 4 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈5

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 4

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Implement the authorized scope: Every task — new code, a fix, a restructuring — climbs the restraint ladder in LADDER.md before each …

    line 53
  2. Very specific:

    Run routine checks inline.

    line 136
  3. Distinctive:

    Start at the required entry point and its callers.

    line 142
  4. Very specific:

    Before the first production edit, turn the documentation and the request into a table-driven test in the package — a new …

    line 150
  5. Very specific:

    Not its Go version: the current form at the module's go directive is written even beside an older neighbor (Write Current Go).

    line 213
  6. Distinctive:

    When the Contract Table passes, walk the diff once, top to bottom, and delete what the test already proves or the code already shows.

    line 222
  7. Distinctive:

    Read the changed path from its entry point to its result or side effect.

    line 256
  8. Very specific:

    Leave complexity headroom: Before a function grows or two merge, read the service's complexity linters in its golangci config (gocyclo, …

    line 353
  9. Distinctive:

    Every owner in Route Before The First Edit.

    line 387

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Go Code Profile

Route Go work to the relevant owners, then close with their verification gate.

Compatibility: Baseline Go 1.27 (see COMPATIBILITY.md).

Resource Routing

Sibling skills resolve relative to this installed directory; scripts run from the target project. A skill is loaded when its SKILL.md is in context: the Skill tool where the host has one, else a read of ../<name>/SKILL.md. Report missing resources.

  • ../go-style-core/SKILL.md — Load on every task (step 2); it carries the idiom card, and its references are read only for a decision the task requires.
  • ../go-linting/SKILL.md — With a shell, load alongside go-style-core at step 2; its gate and receipt verifier close step 6.
  • references/INTENSITY.md — Read when the prompt carries lite or ultra (/go-code ultra <task>, ultra mode): what each level changes.
  • references/NEW-CODE-EXAMPLES.md — Read when the shape of a Contract Table case, Plain Code body, or budgeted helper is in doubt.
  • ../go-code-refactor/references/LADDER.md — Read before the first edit on every task, unless ## The Restraint Ladder is already in context (the plugin's session-start hook prints the whole file): step 5 climbs it for each added entity.
  • ../go-code-refactor/references/OVER-ENGINEERING.md — Read its replacement table only when seeking a simpler existing API.

Workflow

  1. Resolve invocation. $go-code <task> or /go-code <task> selects Go work; a leading lite, full, or ultra sets the restraint level (INTENSITY.md), not the task. An invocation inserts this file only; steps 2 and 3 load the rest. As a modifier (/opsx:apply add-auth /go-code) it is never a change name or path, and the host keeps workflow state, checkpoints, and delegation policy.
  2. Load go-style-core and read the code. With a shell, load go-linting in the same initial Skill message. go-style-core carries the idiom card. Then inspect repository instructions, go.mod (its go directive sets the idiom), neighboring code and tests. Without a shell tool, step 6 runs nothing and go-linting stays unread. A new function, package, or stub body makes step 4 apply. A fix skips the table but adds one case that fails on the code before it and passes after (go-troubleshooting), so go-testing is loaded for it too; a restructuring skips both.
  3. Load the owners before the first edit. Match the task against Route Before The First Edit and load each matched owner plus every Also load entry whose condition holds, all in one message. When step 4 applies or the task is a fix, go-testing is one of them. Select by the decisions being changed: a routine local variable or if triggers no owner. No edit before every selected skill is in context; add owners when new evidence requires them. Wait for successful Skill results before the edit message; do not dispatch loads and edits together.
  4. Write the Contract Table, for new code only. Turn the documentation and the request into the test file the Contract Table describes before the first production edit: a case written after the body checks only what the body already does. Its cases are what step 6 runs or reads.
  5. Implement the authorized scope. Every task — new code, a fix, a restructuring — climbs the restraint ladder in LADDER.md before each helper, type, layer, interface, option, or import it adds, once per entity, and a fix lands at the root cause the ladder names. New code takes the Plain Code form, counts every package-level declaration that survives the ladder in the Declaration Budget, and gets the Delete Pass and Reader Pass once its cases pass. Restructuring follows the delete-first priority. Preserve required behavior, validation, security controls, and meaningful tests. A task that does not ask for a dependency adds none: go.mod stays as it is, and a test compares with the standard library (go-testing owns the assertion rule). Resolve routine choices without stopping; ask only for missing information that changes correctness, scope, or authorization.
  6. Verify and report. With a shell, load the go-linting skill (Skill tool with the router's namespace, otherwise read its SKILL.md), select the gate scope, then run the Contract Table file with that gate. Hook receipts count only as go-linting verifies them. Without a shell, read each case against its code path. Report as go-style-core says, in this shape and order:

    <what the change does, in one sentence>
    checks: gofmt pass · vet pass · test -race pass · lint pass
    added package-level declarations: 1 — parseLimit: handleList, handleSearch
    <one sentence per material gap, or nothing>

    Name the test file and result, not each case.

Route Before The First Edit

The third column adds owners only under its stated condition. Prefer the narrower owner when topics overlap; a local or unexported identifier alone does not require go-naming or go-documentation.

Task touchesSkillAlso load
errors, wrapping, errors.Is/errors.AsTypego-error-handling—
goroutines, channels, mutexes, races, timers and tickersgo-concurrencygo-context if anything is cancelled
context.Context, timeouts, cancellationgo-contextgo-concurrency if goroutines are started
tests, table-driven cases, synctestgo-testing—
API naming changes or a naming questiongo-naming—
new or changed exported API, doc commentsgo-documentationgo-naming if API names change; go-functions if an existing exported signature, interface, or struct changes
interfaces, embedding, test doubles, pointer vs value receiversgo-interfaces—
function API design, constructor configuration, ordering, signatures, Printf helpersgo-functions—
slices, maps, arrays, setsgo-data-structures—
declaration, enum, or initialization decisionsgo-style-core references—
loop/switch mechanics or statement scoping decisionsgo-style-core references—
type parameters, constraints, generic methodsgo-generics—
slog, log levels, request-scoped fields, metrics and trace correlationgo-logginggo-security if a secret or PII could reach a log line
panic/recover and Must helpers, a deferred closure or a written file's Close, boundary copies, mutable globals, nil/aliasing/overflow traps, time.Time/Duration valuesgo-defensive—
a cache, hot paths, allocations, benchmarksgo-performancego-troubleshooting if the cause of slowness is unknown
package layout, imports, dependenciesgo-packages—
restructuring or deleting existing codego-code-refactor—
linter config, CI checksgo-linting—
HTTP handlers, routing, middleware, servers, clients; gRPC services and clientsgo-http (GRPC.md for gRPC)go-security if input reaches a file, shell, URL, or template
SQL queries, transactions, repositories, migrationsgo-databasego-security if a request value picks a row by ID or names a column, table, or sort order
untrusted input, secrets, tokens, TLS, cookiesgo-securitygo-defensive
retries, idempotency, circuit breakers, overload, backpressure, fallbackgo-resiliencethe HTTP, SQL, context, or concurrency owner when its mechanics change
ticket, wrong result, environment regression, panic, hang, leak, flaky test; cause unknowngo-troubleshootingthe owner of the mechanism once found
JSON and other wire formats, struct tagsgo-http (JSON-V2.md)go-defensive (tags)
CLI entry point, flags, main/rungo-packages—
a list of findings from a review or auditthe rows the findings nameper area, not per task

Close With The Gate

Select checks with go-linting for the requested scope; a repository gate replaces the defaults rather than joining them. Bundled scripts add evidence the gate lacks:

  • Error handling: bash <installed-skill-dir>/../go-error-handling/scripts/check-errors.sh ./....
  • Exported API documentation: bash <installed-skill-dir>/../go-documentation/scripts/check-docs.sh.
  • Before submitting: go-code-review.

<installed-skill-dir> is the directory the host loaded this file from; each command runs from the project with that path written out, through bash: an install can drop the scripts' executable bit.

Run routine checks inline. The report carries only results observed for the current diff and scope, each as pass, fail, unavailable (reason), or skipped (reason) per go-linting.

Writing New Code

Start at the required entry point and its callers. Implement the documented success and failure paths within the existing API; design a new API only when the task calls for one. A stub's panic("not implemented") is missing behavior, not a refactor contract: replace it and satisfy the acceptance tests.

Contract Table

Before the first production edit, turn the documentation and the request into a table-driven test in the package — a new <pkg>_contract_test.go, or cases added to the existing test file — one case per observable clause of the documentation, not per branch of the body (go-testing says which cases earn a place): each request or input class, each method and status code, ordering, the error a caller matches and any error text the documentation promises, and the empty, nil, and invalid inputs; a clause that says what the code must not do is a case too. The empty case is built from nil — a nil slice, a nil map, a request with no body — not from an empty literal: []T{} already has the shape the case is meant to prove, and slices.Clone of nil is nil. A case for a list that may come back empty compares the body text with [] (strings.TrimSpace(rec.Body.String())), never a decoded value: json.Unmarshal decodes null to a nil slice and [] to an empty one, both of length zero, and the case passes on the one body the contract forbids. A clause written as a class — "any other method", "any other value" — takes its case from the member a library default treats unlike the rest: for "any method other than GET" that is one HEAD request per GET path, the health check included, since a GET pattern answers HEAD with 200 on its own.

Where a case contradicts a standard-library default — a nil slice encoding as null, a GET pattern answering HEAD with 200 — the contract wins, and the default is overridden in code rather than explained in prose. A failing or unrunnable case is reported as such, never deleted or weakened to pass; an expectation that turns out wrong is corrected against the documentation, not against the implementation. The file is plain code too: one case struct, one table, one t.Run loop, and a failure message in the Func(input) = got, want form; go-testing owns the table-test form, and NEW-CODE-EXAMPLES.md shows a worked table.

Plain Code

The body reads as the documentation reads: reject invalid states early (the early return), then do the main work in its natural order. Return directly when that makes the result clear; do not introduce a result variable to enforce one final return. This applies to the entry point, anything it calls, and the contract test.

  • Vocabulary from the specification. The doc comment's nouns name the variables.
  • Smallest scope that works. A document built in one function is an anonymous struct or a type declared inside that function; a step done once is inline unless it sits at another level of abstraction (Declaration Budget rule 4); an error carrying context is fmt.Errorf("op %q: %w", key, err), matched by the caller with errors.Is.
  • Names that explain, states that stay. An intermediate value gets a name when its expression nests deeper than one call or the name says what the expression does not; otherwise it is written where it is used. A fact the code tracks — what was already asked for, what was sent, what failed — keeps its own variable even when another one almost holds it.
  • Use a standard-library operation when it states the intent more clearly (the idiom card lists them); a loop that tracks several related facts may read better as a loop. Check ordering, ownership, and nil-versus-empty results. Reach For What Go Ships lists replacements; go-data-structures owns collection semantics.
  • New JSON is encoding/json/v2. A package with no encoding/json import writes import json "encoding/json/v2" (Go 1.27) and no make([]T, 0, n) for the wire; one on v1 keeps v1. JSON-V2.md owns what each writes for nil.
  • Neighbors set the register. Where the package has code, match its naming and comment density (House Style). Not its Go version: the current form at the module's go directive is written even beside an older neighbor (Write Current Go).

NEW-CODE-EXAMPLES.md carries a whole documented JSON document in this form.

Delete Pass

When the Contract Table passes, walk the diff once, top to bottom, and delete what the test already proves or the code already shows. Each item below is a line a reviewer sends back:

  • A comment that narrates the next line or repeats what the code already shows. A comment giving a reason the code cannot show stays (go-style-core owns comments).
  • A name used once that explains nothing, and the variable it fills: the value is written where it is used. A name that passes the Plain Code test stays.
  • A field, header, or option set to what the library uses when it is absent: MaxHeaderBytes: 1 << 20 is http.DefaultMaxHeaderBytes, Content-Type: text/plain; charset=utf-8 before w.Write([]byte("ok")) is what the writer sniffs for a body the code fixes — a body that carries input keeps its Content-Type (go-security) — and false, 0, nil, or "" in a keyed literal is the zero value. The line says nothing its absence does not.
  • A fmt.Errorf wrap that only restates the call: return json.Marshal(doc). The failure branch stays, since json.Marshal fails on input (invalid UTF-8 or a time.Duration under v2, NaN under v1) and a handler keeps its 500; a branch on a call documented never to fail — (*bytes.Buffer).Write, (*strings.Builder).WriteString, hash.Hash.Write — goes. A write whose error has nowhere to go is discarded in the open, _, _ = w.Write(body) with its reason, never bare: errcheck in the gate reads a bare call as a finding, and the reason on that line is not a comment to delete.
  • A doc comment on an unexported helper beyond one line; go-documentation owns the exported ones.

The pass runs behind the test, never ahead of it: a line a case needs stays, and validation at a trust boundary, failure behavior, and security controls are not deleted for a smaller diff. Rerun or reread the cases after it.

Reader Pass

Read the changed path from its entry point to its result or side effect. Can a reader find the main decision, error exits, and state the code tracks without chasing wrappers that only rename an operation? Keep a one-use name when it exposes a meaningful boundary or state; keep a helper when it hides details that obscure the caller's decision. If either adds a jump without answering a reader's question, inline it and run the cases again.

Declaration Budget

The specification already declares what the task needs: a body behind an existing signature adds no package-level declaration by default, and the count of those it does add is what the report carries. Count every function, method, type, interface, and variable added at package level in production code across the whole diff; go-testing owns test helpers. A declaration is written when the code shows the need at the moment it is written:

  1. Two call sites exist in the diff, and the report names both.
  2. A caller outside the function names it: it reads a field through errors.AsType, satisfies an interface a consumer declares (go-interfaces owns the shape), or owns a resource whose lifetime outlives one call. A caller that inspects or matches an error reads no field: it uses errors.Is on the wrapped sentinel, and that needs no type.
  3. It is a distinct algorithm — a parser, a scheduler — whose name at the call site says more than its body would, and the body left behind reads top to bottom without it.
  4. It is a step at another level of abstraction than the rest of the function — building SQL beside the domain rule — even with one call site; the report names its caller and this rule. A helper whose name only restates two or three lines of its body is not such a step: those lines stay inline. In multi-step business rules the parse, the pure match, and the interval arithmetic are such steps (Business Rules).

A function literal bound to a name that captures nothing from the function around it counts under the same four rules: the count is a record, not a score, and hiding a helper inside a function changes the number without changing the code. A closure is for capturing state, and a handler registered once is written at its registration; the values routes share are locals of the constructor that the handler literals capture, not fields of a server type with one method per route.

A representation is a value, not a reason: a wire document, a formatted error, and a sorted view take the Plain Code form. A helper that names a step at its caller's own level of abstraction meets none of the four; write the step inline. The named helpers in skill examples (validate, writeJSON, writeError) show the order of an operation, not a list to reproduce.

The budget counts what survives the restraint ladder in LADDER.md, climbed per declaration before the four rules are checked.

The report carries one line — added package-level declarations: N — and, for each, its name and the call sites or caller that need it. A declaration whose line cannot name them is removed before the report is written, not explained in it.

Business Rules

Code that carries multi-step business rules — intervals, history or snapshot processing, reconciling against a set of properties, branching on mode or role — is written as a chain, new or refactored; plain CRUD and glue code are exempt.

  • The entry point is the chain. Each step's result is a named value the next step takes: a := step1(), b := step2(a), c := step3(a, b). The whole algorithm and its key branches read in that function or one level below it. A struct holding the call's dependencies (ctx, tx, the input) may carry the steps as methods; a fluent chain that passes intermediate results through struct fields hides the data flow and is not written.
  • Effects at the top, logic below. Deep functions return data; writes and external calls happen in the entry point or a step of its chain. Lifting an effect keeps the order and count of writes (BEHAVIOR-TRAPS.md).
  • Untyped data is parsed once. A map, a JSON document, or a database row becomes a typed struct in one place, which returns the parse error; the decisions and comparisons after it are pure functions with no error result. Checking the value types and converting them is that one pass, not a validation loop followed by a second switch v.(type) in the matcher: the matcher takes the typed struct, never the map[string]any, and holds no type switch or assertion.
  • A step earns its function by the Declaration Budget: a step of the entry point's chain, or a pure step below it, is rule 4's other level of abstraction even with one call site. Three steps are named here because a body written behind one signature folds them in: the parse of untyped data into its typed struct, the pure match or decision on that struct, and the interval or set arithmetic. Each is a package-level function, not a closure inside the entry point, which keeps the step names, the branches, and the writes and stays within about 40 lines. Any other wrapper around one expression or one error check is inlined. Its name says what the step means in the domain, not the library call behind it: verb-like for an action or a predicate (subtract, matches), noun-like for a value it returns (processingInterval); leftDifferences names the mechanism and processReminder names nothing (go-naming owns the conventions).
  • Leave complexity headroom. Before a function grows or two merge, read the service's complexity linters in its golangci config (gocyclo, gocognit, funlen, nestif) and stay clear of their limits, not at them.

A fragment of an entry point in that shape; b holds only the dependencies, bindCombination writes by snapshot and returns what it left uncovered, and inside it readSnapshot parses each snapshot once so matches stays pure:

	interval, ok, err := b.processingInterval()
	if err != nil || !ok {
		return false, err
	}
	snapshots, err := b.loadSnapshots(interval)
	if err != nil {
		return false, err
	}
	remainders, err := b.bindCombination(interval, snapshots)
	if err != nil {
		return false, err
	}
	if !data.Bind {
		return true, nil // unbind leaves values outside the combination alone
	}
	for _, r := range remainders {
		if err := b.clear(r); err != nil {
			return false, err
		}
	}
	return true, nil

Related Skills

  • Every owner in Route Before The First Edit.
  • go-style-core: house style, report length, and delegation.
  • go-linting: the closing gate.
  • go-code-review: the checklist before submitting.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.