Go Idioms skill for Codex

Go that reads like Go, not Java in disguise. Codex applies Go's own conventions instead of patterns imported from other languages: errors wrapped once with context and inspected with errors.Is or errors.As, interfaces declared by their consumers, packages split by domain with names that read well at the call site, and useful zero values. Mechanical upgrades go through go fix and golangci-lint v2 before any hand edits.

  • Languages
  • $go-idioms
  • 191 lines
  • 2 files
  • ~1,850 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- go-idioms

Installs to ~/.agents/skills/go-idioms/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $go-idioms, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Writes and reviews idiomatic Go - error wrapping with %w, errors.Is and errors.As, sentinel versus typed errors, small consumer-defined interfaces, package layout and naming without stutter, useful zero values, functional options, generics where they pay off, and current standard library idioms applied with go fix modernizers, go vet, staticcheck and golangci-lint v2. Use when a user asks to make Go code more idiomatic, review a Go PR for style, design an error strategy, structure packages, or clean up Java-style abstractions in Go.

Use it when

  • Go code carries Java or Python habits such as getters, deep interfaces or util packages
  • Error handling logs and returns at every level or compares strings
  • You are reviewing a Go pull request for style and API design
  • A codebase should adopt current Go idioms after a toolchain upgrade

Use something else when

  • The question is goroutines, channels or data races: use go-concurrency
  • You need go.mod, workspaces or versioning help: use go-modules

How to check the result

gofmt reports nothing, go vet and golangci-lint pass, tests pass, and every returned error in the diff is either wrapped with context or deliberately hidden.

The mistake it prevents

Returning a typed nil pointer as an error interface, which makes err != nil true even though nothing failed.

~/.agents/skills/go-idioms/SKILL.md

--- name: go-idioms description: Writes and reviews idiomatic Go - error wrapping with %w, errors.Is and errors.As, sentinel versus typed errors, small consumer-defined interfaces,… license: MIT ---

Done well means: errors carry context and stay inspectable, interfaces are small and live with their consumers, package names read well at the call site, zero values work, and go vet plus the project’s linters pass with no new suppressions.

Before changing anything

  • Read AGENTS.md, go.mod (the go directive decides which language features are legal), .golangci.yml, and the Makefile or task runner for the lint and test commands.
  • Run the baseline: go build ./... && go vet ./... && go test ./....
  • Let tools do mechanical upgrades first. On Go 1.26+, go fix ./... applies the modernizers (for example for i := range n, min/max, slices and maps helpers); review its diff separately from hand edits. On older toolchains, gopls reports the same modernize hints.

Errors

SituationPattern
Add context while passing upreturn fmt.Errorf("load config %s: %w", path, err)
Caller must branch on a conditionSentinel: var ErrNotFound = errors.New("user not found"), checked with errors.Is
Caller needs data from the errorTyped error struct, checked with errors.As
Several independent failureserrors.Join(errs...)
Implementation detail callers must not depend onWrap with %v, not %w, to hide it
Truly impossible statepanic with a clear message; never for expected failures
type ValidationError struct {
    Field  string
    Reason string
}

func (e *ValidationError) Error() string { return e.Field + ": " + e.Reason }

func (s *Service) Create(ctx context.Context, u User) error {
    if u.Email == "" {
        return &ValidationError{Field: "email", Reason: "required"}
    }
    if err := s.repo.Insert(ctx, u); err != nil {
        return fmt.Errorf("insert user %q: %w", u.Email, err)
    }
    return nil
}

// caller
var ve *ValidationError
switch {
case errors.As(err, &ve):
    http.Error(w, ve.Error(), http.StatusBadRequest)
case errors.Is(err, ErrNotFound):
    http.NotFound(w, r)
case err != nil:
    log.Error("create user", "err", err)
    http.Error(w, "internal error", http.StatusInternalServerError)
}

Rules:

  • Handle an error once: either log it or return it, not both. Logging at every level produces five copies of one failure.
  • Context strings are lowercase, no trailing punctuation, no “failed to” (every error failed): open db: dial tcp: connection refused reads as a chain.
  • Never compare error strings. Never errors.Is(err, &MyErr{}) for typed errors; use As.
  • Check errors from Close on writers (files, buffered writers, HTTP request bodies you created); defer-with-named-return when it matters.

Interfaces

  • Define interfaces where they are used, not where they are implemented. The consumer declares the 1-3 methods it needs.
  • Return concrete types from constructors; accept interfaces in parameters.
  • No interface until there are two implementations or a test needs a fake.
  • Name single-method interfaces after the method plus -er: Reader, Notifier.
  • Assert implementation at compile time when it matters: var _ Store = (*PGStore)(nil).
// package billing (consumer)
type invoiceStore interface {
    Invoice(ctx context.Context, id string) (Invoice, error)
}

type Service struct{ store invoiceStore }

func NewService(store invoiceStore) *Service { return &Service{store: store} }

Packages and naming

RuleExample
Package names: short, lowercase, singular, no underscoresuser, httpx, not user_utils
No util, common, helpers, models dumping groundsPut code with what it serves
Avoid stutteruser.Service, not user.UserService; http.Client, not http.HTTPClient
Initialisms keep caseuserID, ServeHTTP, parseURL
Receivers: 1-2 letters, consistent, never self/thisfunc (s *Server) Start()
Getters without Getu.Name(), setter u.SetName()
internal/ for code other modules must not importinternal/platform/db
cmd/<binary>/main.go for executablesthin main, logic in packages

Layout: start flat. A small service is main.go plus a few files in one package. Split by domain (order, payment), not by layer (controllers, services, repositories).

Zero values and construction

  • Make the zero value useful: var mu sync.Mutex, var buf bytes.Buffer, a struct whose nil map is lazily initialized.
  • Constructors (NewX) only when construction needs validation or non-zero defaults.
  • Few optional settings: a config struct. Many, evolving optional settings in a public API: functional options.
type Option func(*Client)

func WithTimeout(d time.Duration) Option { return func(c *Client) { c.timeout = d } }

func NewClient(baseURL string, opts ...Option) *Client {
    c := &Client{baseURL: baseURL, timeout: 10 * time.Second}
    for _, o := range opts {
        o(c)
    }
    return c
}

Generics: when they pay off

Use type parameters for container types and algorithms that are identical across types (slices.SortFunc, a typed cache, Map/Filter helpers when you really need them). Do not use them to emulate inheritance or where an interface method call is clearer. Prefer the standard slices, maps, cmp packages over hand-written helpers.

Current idioms worth applying

OldCurrent
for i := 0; i < n; i++ with unused complexityfor i := range n (1.22)
Copying loop variable v := vNot needed since 1.22 per-iteration semantics; remove
Hand-written contains, index, sort wrappersslices.Contains, slices.Index, slices.Sort
Map key collection loopsslices.Sorted(maps.Keys(m)) (1.23 iterators)
if a < b { return a } helpersmin, max builtins (1.21)
Zero-out maps by loopingclear(m) (1.21)
First non-zero value chainscmp.Or(a, b, c) (1.22)
log.Printf structured-ish logslog/slog with key-value pairs
ioutil.*io and os equivalents
context.Background() in testst.Context() (1.24)

Lint configuration

# .golangci.yml (golangci-lint v2 format)
version: "2"
linters:
  default: standard
  enable: [errorlint, revive, misspell, unconvert, gocritic, bodyclose, nilerr]
formatters:
  enable: [gofumpt, goimports]

errorlint catches == comparisons and %v wrapping where %w was intended.

Gotchas

  • A nil pointer stored in an interface is a non-nil interface. Returning (*MyErr)(nil) as error makes err != nil true. Return a literal nil.
  • defer in a loop runs at function exit, not iteration end; extract a function.
  • Slices share backing arrays: append to a sub-slice can overwrite the parent. Use slices.Clone or full slice expressions s[a:b:b] when handing out sub-slices.
  • Maps are not safe for concurrent writes; see $go-concurrency.
  • Exported names are API. Renaming an exported identifier in a library is a breaking change needing a major version.
  • init() functions with side effects make tests and imports unpredictable; prefer explicit setup.

Verification

gofmt -l . | tee /dev/stderr | (! read)    # no unformatted files
go vet ./...
golangci-lint run ./...                    # if configured
go test ./...

Review checklist for the diff: every returned error is wrapped or deliberately not; no new util package; interfaces declared at the consumer; no exported name changed without calling it out.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Go Idioms"
  short_description: "Go that reads like Go, not Java in disguise"
  default_prompt: "$go-idioms Review this Go package and make it more idiomatic"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem