Skip to content
Development
Agent

go-modern-code

Load this file before you write or edit any Go code. Each rule below fixes a failure measured in generated code: code that built and passed its tests but used outdated or weak idioms. Rules marked with a version need that version or later in `go.mod`; check the `go` line first.

From plugin
vexjoy-agent
425198 skills198 agents12 commands78 hooks
Install
$ npx -y skills add notque/vexjoy-agent --agent claude-code

How it fires

How this agent gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.

Context preview

The summary Claude sees to decide when to auto-load this agent.

Load this file before you write or edit any Go code. Each rule below fixes a failure measured in generated code: code that built and passed its tests but used outdated or weak idioms. Rules marked with a version need that version or later in `go.mod`; check the `go` line first.

Agent definition

go-modern-code.md

Writing Modern Go (Go 1.27)

Load this file before you write or edit any Go code. Each rule below fixes a failure measured in generated code: code that built and passed its tests but used outdated or weak idioms. Rules marked with a version need that version or later in `go.mod`; check the `go` line first.

grep '^go ' go.mod   # "go 1.27" unlocks every rule here; lower versions: see go-version-idioms.md

1. Doc comments on every exported name

Every exported type, function, method, constant block, and variable block gets a comment that starts with its name. Exactly one non-test file per package starts with a package comment directly above the `package` line; `package main` gets one too. Measured: 10 of 12 unguided outputs skipped doc comments; 6 of 14 guided outputs still skipped the package comment.

// BAD
package userrepo

type Repo struct{ db *sql.DB }

func New(db *sql.DB) *Repo { return &Repo{db: db} }

// GOOD
// Package userrepo stores users in SQLite through database/sql.
package userrepo

// Repo stores users in SQLite. It is safe for concurrent use.
type Repo struct{ db *sql.DB }

// New returns a Repo that uses db. The caller owns db and closes it.
func New(db *sql.DB) *Repo { return &Repo{db: db} }

State concurrency safety, ownership, and zero-value behavior when they matter.

2. Errors

| Do | Not | Since | |----|-----|-------| | `errors.Is(err, io.EOF)`, `errors.Is(err, flag.ErrHelp)`, `errors.Is(err, sql.ErrNoRows)` | `err == io.EOF`, `err == flag.ErrHelp` | 1.13 | | `if e, ok := errors.AsType[*http.MaxBytesError](err); ok {` | `var e *http.MaxBytesError; if errors.As(err, &e) {` | 1.26 | | Sentinels prefixed with the package: `errors.New("userrepo: not found")` | `errors.New("not found")` | - | | Name the package once per message: `fmt.Errorf("create user %q: %w", email, ErrDuplicateEmail)` | `fmt.Errorf("userrepo: create: %w", ErrDuplicateEmail)` (prints `userrepo: create: userrepo: duplicate email`) | - | | Named constants from the driver: `sqlite3.SQLITE_CONSTRAINT_UNIQUE` | Magic numbers: `e.Code() == 2067` | - | | Match a driver's typed error or code | `strings.Contains(err.Error(), "UNIQUE constraint failed")` | - |

Tests use `errors.AsType` too:

se, ok := errors.AsType[*filter.SyntaxError](err)
if !ok {
	t.Fatalf("error %T is not *SyntaxError", err)
}

Typed driver errors: find the type in the module cache, then match it. Example for `modernc.org/sqlite` (verified in v1.59.0 source: `*sqlite.Error` has `Code() int`; codes live in `modernc.org/sqlite/lib`):

import (
	"modernc.org/sqlite"
	sqlite3 "modernc.org/sqlite/lib"
)

func isUniqueViolation(err error) bool {
	e, ok := errors.AsType[*sqlite.Error](err)
	return ok && e.Code() == sqlite3.SQLITE_CONSTRAINT_UNIQUE
}

For other drivers, read the source first: `grep -rn 'func (e \*Error)' $(go env GOMODCACHE)/<driver>@<version>/`.

Wrap with context that names the operation and the input: `fmt.Errorf("create user %q: %w", email, err)`. Return an error unchanged when the callee already says enough.

3. Context

  • Tests: `ctx := t.Context()`. Never `context.Background()` or `context.TODO()` in a test (1.24). Measured: 8 of 12 unguided outputs used `context.Background()` in tests.
  • Library code: accept `ctx` as the first parameter. Never create `context.Background()` inside a library function.
  • Cleanup that must outlive a canceled ctx (HTTP shutdown, rollback): `context.WithoutCancel(ctx)` keeps values and drops cancellation (1.21).
  • First-error cancellation: `context.WithCancelCause` + `context.Cause(ctx)` (1.20) carries the real error; no extra mutex or error box needed.
  • `main`: `signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)` is the one place `context.Background()` belongs.

`os.Exit` skips deferred calls. Never `defer` in a function that calls `os.Exit`; move the work into a function that returns an exit code. Measured: every unguided CLI `main` did this.

// BAD: stop never runs
func main() {
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()
	os.Exit(wordfreq.Run(ctx, os.Args[1:], os.Stdin, os.Stdout, os.Stderr))
}

// GOOD
func main() {
	os.Exit(run())
}

func run() int {
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()
	return wordfreq.Run(ctx, os.Args[1:], os.Stdin, os.Stdout, os.Stderr)
}

4. Goroutines and bounded concurrency

`wg.Go(f)` replaces `wg.Add(1); go func() { defer wg.Done(); ... }()` (1.25). Use it in library code and in tests. `for range n` replaces `for i := 0; i < n; i++` when `i` is unused (1.22). Loop variables are per-iteration since 1.22: delete `i := i` and `tt := tt` copies and closure parameters that only pass the loop variable.

Canonical bounded worker pool. Results go straight into a preallocated slice by index; no results channel, no dispatcher goroutine, at most `workers` goroutines:

// Map calls fn for every item with at most workers calls in flight and
// returns the results in input order.
func Map[T, R any](ctx context.Context, items []T, workers int, fn func(context.Context, T) (R, error)) ([]R, error) {
	if workers < 1 {
		return nil, errors.New("pool: workers must be >= 1")
	}
	ctx, cancel := context.WithCancelCause(ctx)
	defer cancel(nil)

	out := make([]R, len(items))
	var next atomic.Int64
	var wg sync.WaitGroup
	for range min(workers, len(items)) {
		wg.Go(func() {
			for ctx.Err() == nil {
				i := int(next.Add(1) - 1)
				if i >= len(items) {
					return
				}
				r, err := fn(ctx, items[i])
				if err != nil {
					cancel(fmt.Errorf("item %d: %w", i, err)) // first call wins
					return
				}
				out[i] = r // each index written by one goroutine: no race
			}
		})
	}
	wg.Wait()
	if err := context.Cause(ctx); err != nil { // fn error, or parent's context.Canceled
		return nil, err
	}
	return out, nil
}

Every goroutine you start has an owner that waits fo

Read more
Ships withvexjoy-agent

Essays and writing behind this toolkit live at vexjoy.com. VexJoy Agent connects plain-English requests to specialist agents, skills, and workflows. /do selects the knowledge and tools needed for your task.

Get the whole plugin

Other agents on vexjoy-agent.