Skip to content
Skillv1.0.0

go

Use when writing, reviewing, testing, or shipping Go code and HTTP services: idioms, `%w` error wrapping, goroutine/context/errgroup concurrency, net/http 1.22 routing, log/slog, project layout, table

by ericrisco(0) 0 installs
Free
Sign in to install

Free account. Installing gives you the manifest plus copy-paste snippets.

See reviews

About

Imported from ericrisco/rsc-harness (skills/go/SKILL.md). Install upstream with npx skills add ericrisco/rsc-harness --skill go. Copyright stays with the author.

Idiomatic Go services

Targets Go 1.22+ (Go 1.26 is the current stable release): enhanced net/http routing (mux.HandleFunc("GET /users/{id}", h) + r.PathValue), log/slog structured logging, and fixed loop-variable semantics (no more tt := tt).

⚠️ SDD new-feature gate — read this first. If this skill fired on a new, non-trivial feature or behaviour change and there is no approved spec + plan under 02-DOCS/wiki/sdd/, STOP — do not write feature code yet. Hand off to ../specify/SKILL.md first: it runs brainstorm → spec → plan → tasks before any code, then routes back here once the plan is approved. Build here directly only for a genuinely one-line / low-risk change. Method: ../sdd/SKILL.md.

Boundary

Go error handling and HTTP contract design (status-code taxonomy, REST resource naming) live here — this skill is the canonical authority for both in Go. Delegate outward:

  • Language-agnostic abuse/authz review, threat modeling, OWASP-class bugs -> secure-coding. This skill keeps the Go-specific controls: SQL params, server timeouts, govulncheck, TLS defaults.
  • Containerfile / k8s / CI pipeline authoring -> deployment. This skill ships only a Docker note + ldflags.
  • Recording per-project conventions in a workspace wiki -> harness (see "Project grounding" below).

Non-service Go (CLI tooling, codegen, ML): the patterns apply, but the HTTP/production half is irrelevant.

Idioms

Useful zero value. Design types so the zero value works before any constructor.

// Good: zero-value Counter is ready; the zero-value mutex is unlocked. var b bytes.Buffer too.
type Counter struct {
	mu sync.Mutex
	n  int
}

func (c *Counter) Inc() { c.mu.Lock(); c.n++; c.mu.Unlock() }

// Bad: nil map field panics on first write (assignment to entry in nil map); hidden init step.
type Registry struct{ items map[string]int }

func (r *Registry) Add(k string) { r.items[k]++ } // panic if items was never make()'d

Accept interfaces, return structs. Return the concrete type; declare the interface where it is consumed.

type UserStore interface { // declared in package service - only what it needs
	GetUser(ctx context.Context, id string) (*User, error)
}
func NewService(s UserStore) *Service { return &Service{store: s} } // Good: return *Service
// Bad: func NewService(s UserStore) UserStore - returning the interface hides the type.

Functional options. Defaults first, then apply options.

type Server struct {
	addr    string
	timeout time.Duration
	logger  *slog.Logger
}
type Option func(*Server)

func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } }
func WithLogger(l *slog.Logger) Option   { return func(s *Server) { s.logger = l } }

func NewServer(addr string, opts ...Option) *Server {
	s := &Server{addr: addr, timeout: 30 * time.Second, logger: slog.Default()} // defaults first
	for _, opt := range opts {
		opt(s)
	}
	return s
}

Use a plain Config struct once options exceed ~5; options are for optional, composable tuning, not required fields.

Embedding for composition. Embed to borrow a behavior, not to fake inheritance.

// Good: the service gets .Info/.Error for free from the embedded logger.
type Service struct {
	*slog.Logger
	store UserStore
}
// Bad: deep type trees (Base -> Middle -> Leaf) modeling "is-a" inheritance - avoid.

Early return. Clear over clever: invert the error and return; keep the happy path flat (no arrow code).

// Good: each failure returns immediately; the success path is unindented.
func save(ctx context.Context, u *User) error {
	if u == nil {
		return errors.New("nil user")
	}
	if err := validate(u); err != nil {
		return fmt.Errorf("validate: %w", err)
	}
	return store.Put(ctx, u)
}
// Bad: if u != nil { if err := validate(u); err == nil { ... } else { ... } } - arrow code.

No package-level mutable state. Inject via constructor (func New(db *sql.DB) *Server), never a global var db *sql.DB opened in init() - globals couple everything and kill testability.

Receivers. Pick value or pointer per type and stay consistent across its method set; mutating / large / contains-sync -> pointer.

Go 1.22 loopvar. Loop variables are per-iteration now. Stop emitting the workaround: inside for _, tt := range tests the line // tt := tt is obsolete - DELETE it.

Errors

Sentinel vs typed. Sentinels for identity; typed errors for data.

var ErrNotFound = errors.New("not found")         // sentinel: identity
type ValidationError struct{ Field, Msg string }  // typed: carries data
func (e *ValidationError) Error() string { return fmt.Sprintf("%s: %s", e.Field, e.Msg) }

Wrap and classify. Wrap every crossed boundary with %w; never compare message strings.

err := fmt.Errorf("find user %s: %w", id, ErrNotFound)
if errors.Is(err, ErrNotFound) { /* sentinel match through the wrap chain */ }
var verr *ValidationError
if errors.As(err, &verr) { /* typed match: verr.Field, verr.Msg */ }
joined := errors.Join(err1, err2) // 1.20+: aggregate; Is/As traverse both

3-layer boundary (the canonical flow). Repo wraps the driver sentinel into a domain sentinel; service passes it through; handler classifies once and maps to a status, logging only the unexpected.

// repository: translate sql.ErrNoRows into a domain sentinel, keep the chain.
func (r *Repo) GetUser(ctx context.Context, id string) (*User, error) {
	var u User
	err := r.db.QueryRowContext(ctx, "SELECT id, name FROM users WHERE id = $1", id).
		Scan(&u.ID, &u.Name)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, fmt.Errorf("user %s: %w", id, ErrNotFound)
	}
	if err != nil {
		return nil, fmt.Errorf("query user %s: %w", id, err)
	}
	return &u, nil
}

// handler: classify once, map to HTTP status.
func (h *Handler) getUser(w http.ResponseWriter, r *http.Request) {
	u, err := h.svc.GetUser(r.Context(), r.PathValue("id"))
	switch {
	case err == nil:
		writeJSON(w, http.StatusOK, u)
	case errors.Is(err, ErrNotFound):
		http.Error(w, "not found", http.StatusNotFound)
	default:
		slog.Error("get user", "err", err)
		http.Error(w, "internal error", http.StatusInternalServerError)
	}
}

Full handler adapter (error-returning apiHandler) -> references/http-services.md.

defer + named return to capture Close() errors:

func read(name string) (err error) {
	f, e := os.Open(name)
	if e != nil {
		return e
	}
	defer func() { err = errors.Join(err, f.Close()) }() // capture Close() into the return
	return nil
}

Concurrency (essentials)

context.Context is the first param of every call, never stored in a struct, never nil (use context.TODO() while wiring). Bound work with a context deadline; bound concurrency with errgroup — the derived ctx cancels siblings on first error, and g.SetLimit(n) caps in-flight goroutines:

g, ctx := errgroup.WithContext(ctx)
g.SetLimit(8)
for _, id := range ids {
	g.Go(func() error { return process(ctx, id) }) // Go 1.22+: no id := id needed
}
err := g.Wait()

Three rules cover most service code: every goroutine needs a known exit path (a started goroutine you cannot stop is a leak); an unbuffered ch <- v with no receiver after a cancel blocks forever, so buffer it and select on ctx.Done(); run -race in CI. Low-level needs map to sync.Once (lazy init), sync.RWMutex (read-heavy state), sync/atomic (atomic.Int64 counters).

Full implementations — context plumbing, channel/select patterns, leak detection, worker pools, pipelines, fan-in/out, semaphores, singleflight, and a withRetry helper (backoff + full jitter, ctx-aware, never retries 4xx) -> references/concurrency.md.

HTTP services (essentials)

Go 1.22 routed mux — method and path live in the pattern; the error-returning adapter centralizes status mapping:

mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", getUser) // 405 on wrong method, 404 on no match
id := r.PathValue("id")                    // inside the handler

type apiHandler func(http.ResponseWriter, *http.Request) error
func (h apiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if err := h(w, r); err != nil { /* classify via errors.Is/As -> status + slog */ }
}

Set all four http.Server timeouts (ReadHeaderTimeout, ReadTimeout, WriteTimeout, IdleTimeout) — an unbounded read is a Slowloris DoS. Graceful shutdown on signal: signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM), then srv.Shutdown(shutdownCtx) on <-ctx.Done().

Routing patterns, chi vs stdlib, the full middleware chain (request-id, slog, panic-recovery, timeout), config, timeout values, functional-options server, and JSON helpers -> references/http-services.md.

Project layout

cmd/api/main.go        # entrypoint: wiring only
internal/handler/      # HTTP adapters
internal/service/      # business logic; defines the interfaces it needs
internal/repository/   # data access (pgx); implements service interfaces
internal/config/       # env parsing, validation
pkg/                   # ONLY genuinely reusable, stable public API
testdata/              # fixtures, golden files
go.mod go.sum

Wire the layers with constructor injection, outermost depends inward: repo := repository.New(db); svc := service.New(repo); h := handler.New(svc).

Package naming: short, lowercase, no underscores, no util/common, avoid stutter (user.User, not user.UserStruct). Interfaces live on the consumer side: the service package declares UserStore; the repository package implements it without importing the interface.

Testing (essentials)

Table-driven with subtests and parallelism:

func TestParse(t *testing.T) {
	tests := []struct {
		name    string
		in      string
		wantErr error
	}{
		{"ok", "42", nil},
		{"bad", "x", ErrInvalid},
	}
	for _, tt := range tests { // Go 1.22+: no tt := tt needed
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			_, err := Parse(tt.in)
			if !errors.Is(err, tt.wantErr) { // classify, not just != nil
				t.Fatalf("got %v, want %v", err, tt.wantErr)
			}
		})
	}
}

HTTP handlers via httptest: req := httptest.NewRequest("GET", "/users/1", nil); w := httptest.NewRecorder(); h.ServeHTTP(w, req); then assert on w.Code / w.Body.

Use t.Helper() in assertions, t.TempDir() for files, t.Cleanup() for teardown, t.Setenv() for env. Run go test -race -cover ./..., and treat go vet / staticcheck failures as build failures. Stdlib testing is the default; reach for testify/require only for deep-equality or large suites.

Golden files, fuzzing, benchmarks, httptest matrices, interface fakes -> references/testing.md.

Security (embedded)

Validate at the boundary: parametrize SQL (PostgreSQL; prefer pgx v5 over database/sql); cap request bodies and reject unknown fields; set a TLS floor and trust the crypto/tls defaults:

// Good                                       // Bad: string interpolation = SQL injection.
db.QueryContext(ctx, "... WHERE id = $1", id) // db.QueryContext(ctx, fmt.Sprintf("... '%s'", id))

r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MiB cap
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()

tlsCfg := &tls.Config{MinVersion: tls.VersionTLS12} // do not hand-pick cipher suites

Server timeouts are a DoS control - set all four (see HTTP services above).

Run govulncheck ./... in CI; keep deps honest with go mod tidy + go mod verify. Read secrets from env / a secret manager, never log them; redact tokens with slog ReplaceAttr. Deeper authz/abuse review -> secure-coding.

Production

Wire log/slog JSON in main (level from env), then slog.SetDefault:

logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: lvl}))
slog.SetDefault(logger)

Stamp the version with ldflags and read module info at runtime:

go build -ldflags "-X main.version=$(git describe --tags --always)" ./cmd/api
# at runtime: if info, ok := debug.ReadBuildInfo(); ok { slog.Info("build", "go", info.GoVersion) }

Mount net/http/pprof on a separate internal mux/port (never the public listener); expose /healthz (static 200 liveness) and /readyz (calls db.PingContext with a short timeout, 503 on failure). Graceful shutdown as shown above.

Docker note: distroless/static base, CGO_ENABLED=0, multi-stage build. Full Containerfile -> deployment.

Anti-patterns

Anti-pattern Do instead
Storing ctx in a struct to avoid threading it ctx is the first arg of every call.
_ = err because it "can't fail" Handle, log, or document why; errcheck catches it.
String-comparing the error message errors.Is / errors.As; messages are not API.
Global db / logger because it's "simpler" Inject via constructor; globals kill testability.
Fire-and-forget goroutine ("it'll finish") Unbounded/unstoppable goroutine = leak; give it ctx + buffer.
tt := tt added "to be safe" Go 1.22 fixed loopvar; it's noise now.
Interface in the provider package, returning the interface Return structs; interface lives with the consumer.
No timeouts because "the LB handles it" Set all four http.Server timeouts; Slowloris is real.
panic on bad input Return an error; panic only for programmer bugs / main wiring.
Skipping -race because tests pass Race bugs are silent; -race in CI is mandatory.
fmt.Sprintf into SQL on "trusted" input Parametrize ($1...); trust nothing at the boundary.
testify everywhere Stdlib first; reach for testify only when it earns its weight.

Toolchain gate

Task Command
Format gofmt -w . / goimports -w .
Vet go vet ./...
Lint staticcheck ./... / golangci-lint run
Test (race+cover) go test -race -cover ./...
Fuzz go test -fuzz=Fuzz -fuzztime=30s
Vulns govulncheck ./...
Local gate ./scripts/verify.sh (run in your module root)

Project grounding (02-DOCS)

In a project with a 02-DOCS/ layer (the harness Karpathy wiki), this project's service decisions live in 02-DOCS/wiki/stack/go.md, indexed from 02-DOCS/wiki/index.md (the Knowledge map; root CLAUDE.md keeps only a short pointer to it). Read it first on every use and stay consistent. If it is missing or stale, write the project's real choices there — the project layout, the router (stdlib 1.22 / chi), the error and slog conventions, concurrency/timeout defaults — index it, and bump its Updated date in the same change.

No 02-DOCS/ layer? Skip silently (optionally suggest harness). Unlike the brand study, technical conventions are recorded, not gated — never block the task on this.

Use it

Copy one of these into your project. Installing also returns the manifest and these snippets.

yaml
targets:
  - https://api.opensmartroute.ai/api/v1/registry/ericrisco-rsc-harness-go/manifest   # or paste the manifest below

Manifest

An Open Capability Manifest: the router reads it to know what this does, what it costs and when to pick it.

ericrisco-rsc-harness-go.ocm.jsonjson
{
  "ocm": "1",
  "id": "ericrisco-rsc-harness-go",
  "kind": "skill",
  "name": "go",
  "description": "Use when writing, reviewing, testing, or shipping Go code and HTTP services: idioms, `%w` error wrapping, goroutine/context/errgroup concurrency, net/http 1.22 routing, log/slog, project layout, table-driven tests, Go hardening. NOT language-agnostic threat modeling (that is `secure-coding`), NOT Dockerfile/CI shipping (that is `deployment`).",
  "publisher": "ericrisco",
  "version": "1.0.0",
  "capabilities": {
    "domains": [
      "coding",
      "data_analysis"
    ],
    "tags": [
      "skill-md",
      "go",
      "golang",
      "http",
      "backend",
      "service",
      "github"
    ],
    "languages": [
      "en"
    ]
  },
  "quality_prior": 0.6,
  "examples": [
    "Use when writing, reviewing, testing, or shipping Go code and HTTP services: idioms, `%w` error wrapping, goroutine/context/errgroup concurrency, net/http 1.22 routing, log/slog, project layout, table-driven tests, Go hardening. NOT language-agnostic threat modeling (that is `secure-coding`), NOT Dockerfile/CI shipping (that is `deployment`)."
  ],
  "primary": false,
  "metadata": {
    "source": {
      "provider": "github",
      "repository": "https://github.com/ericrisco/rsc-harness",
      "path": "skills/go/SKILL.md",
      "ref": "8cc4716ea549275ade1590ad270da01bdf837ab5",
      "url": "https://github.com/ericrisco/rsc-harness/blob/8cc4716ea549275ade1590ad270da01bdf837ab5/skills/go/SKILL.md",
      "key": "ericrisco/rsc-harness/skills/go/SKILL.md"
    }
  },
  "instructions": "# Idiomatic Go services\n\nTargets **Go 1.22+** (Go 1.26 is the current stable release): enhanced `net/http` routing\n(`mux.HandleFunc(\"GET /users/{id}\", h)` + `r.PathValue`), `log/slog` structured\nlogging, and fixed loop-variable semantics (no more `tt := tt`).\n\n> **⚠️ SDD new-feature gate — read this first.** If this skill fired on a **new, non-trivial feature or behaviour change** and there is **no approved spec + plan** under `02-DOCS/wiki/sdd/`, STOP — do **not** write feature code yet. Hand off to `../specify/SKILL.md` first: it runs brainstorm → spec → plan → tasks before any code, then ro",
  "cost": {
    "context_tokens": 3748
  }
}

Fetch it by URL: GET /api/v1/registry/ericrisco-rsc-harness-go/manifest?version=1.0.0

Reviews

Star ratings from people who tried it. One review per account; edit yours any time.

No reviews yet. Install it, try it, and be the first to rate it.