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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- go-idioms Installs to ~/.agents/skills/go-idioms/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install go-idioms The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project go-idioms Installs to .agents/skills/go-idioms/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/go-idioms.zipunzip -o go-idioms.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) go-idioms PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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.
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(thegodirective decides which language features are legal),.golangci.yml, and theMakefileor 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 examplefor i := range n,min/max,slicesandmapshelpers); review its diff separately from hand edits. On older toolchains, gopls reports the same modernize hints.
Errors
| Situation | Pattern |
|---|---|
| Add context while passing up | return fmt.Errorf("load config %s: %w", path, err) |
| Caller must branch on a condition | Sentinel: var ErrNotFound = errors.New("user not found"), checked with errors.Is |
| Caller needs data from the error | Typed error struct, checked with errors.As |
| Several independent failures | errors.Join(errs...) |
| Implementation detail callers must not depend on | Wrap with %v, not %w, to hide it |
| Truly impossible state | panic 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 refusedreads as a chain. - Never compare error strings. Never
errors.Is(err, &MyErr{})for typed errors; useAs. - Check errors from
Closeon 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
| Rule | Example |
|---|---|
| Package names: short, lowercase, singular, no underscores | user, httpx, not user_utils |
No util, common, helpers, models dumping grounds | Put code with what it serves |
| Avoid stutter | user.Service, not user.UserService; http.Client, not http.HTTPClient |
| Initialisms keep case | userID, ServeHTTP, parseURL |
Receivers: 1-2 letters, consistent, never self/this | func (s *Server) Start() |
Getters without Get | u.Name(), setter u.SetName() |
internal/ for code other modules must not import | internal/platform/db |
cmd/<binary>/main.go for executables | thin 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
| Old | Current |
|---|---|
for i := 0; i < n; i++ with unused complexity | for i := range n (1.22) |
Copying loop variable v := v | Not needed since 1.22 per-iteration semantics; remove |
Hand-written contains, index, sort wrappers | slices.Contains, slices.Index, slices.Sort |
| Map key collection loops | slices.Sorted(maps.Keys(m)) (1.23 iterators) |
if a < b { return a } helpers | min, max builtins (1.21) |
| Zero-out maps by looping | clear(m) (1.21) |
| First non-zero value chains | cmp.Or(a, b, c) (1.22) |
log.Printf structured-ish logs | log/slog with key-value pairs |
ioutil.* | io and os equivalents |
context.Background() in tests | t.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)aserrormakeserr != niltrue. Return a literalnil. deferin a loop runs at function exit, not iteration end; extract a function.- Slices share backing arrays:
appendto a sub-slice can overwrite the parent. Useslices.Cloneor full slice expressionss[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.yaml
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.