Skip to content
cccskills
FreePublished on GitHub

go

Idiomatic Go - package and interface design, error wrapping, table-driven tests, generics, the modern standard library (slices/maps/cmp/errors.Join), current syntax, and logging discipline. Use when writing, reviewing, or refactoring any Go code, especially code drifting toward Java/Spring shapes (deep layer trees, generic repositories, heavy frameworks) - even if the user never says "idiomatic". Distilled from spf13/go-skills and adapted to this repo.

The English summary is being prepared. Showing the original description.

See how to install

Files included (1)

  • SKILL.md10.0 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Idiomatic Go

Write Go that is boring in the best way - direct, predictable, obvious on the first read. Clear is better than clever. The common failure mode (and the LLM default) is importing Java/Spring habits - deep layer trees, generic repositories, heavy frameworks, manual worker pools - into a language built for flat, simple, discoverable APIs. When in doubt, delete the abstraction.

Core principles

Clear is better than clever. If a function's control flow takes three reads to follow, rewrite it. Keep the happy path un-indented: handle errors and edge cases first and return.

func GetUser(id string) (*User, error) {
    user, err := db.FindUser(id)
    if err != nil {
        return nil, fmt.Errorf("finding user %s: %w", id, err)
    }
    return user, nil // happy path stays at the left margin
}

Make the zero value useful. Design types so the zero value works without a constructor - sync.Mutex and bytes.Buffer are the gold standard.

type Counter struct {
    mu    sync.Mutex // usable at the zero value - no New() needed
    count int
}

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

Packages: flat, named for what they do

Start flat; add a package only when a domain earns a namespace. Name packages by what they do (auth, billing, jobs), never by what layer they are (service, repository, controller) - layer-named packages breed circular imports and interface bloat and add zero clarity in Go. No utils/, helpers/, or common/ junk drawers; they signal unclear ownership. Packages don't import each other sideways - cycles mean wrong boundaries; the composition root wires them.

In this repo: the two public surfaces are server/ (build an A2A agent) and client/ (call one), with types/ for the shared A2A wire types and server/config/ for configuration. That is the established structure - follow it; don't invent a service/repository layer split. spf13's transferable rules still hold: name by purpose, no junk-drawer packages, wire at the composition root (here, the A2AServerBuilder / AgentBuilder).

Interfaces: discovered, not designed

Write concrete types first. Introduce an interface only when a consumer genuinely needs to swap implementations, and define it in the consuming package, kept small (io.Reader, not *os.File). Accept interfaces, return structs - callers get the narrow dependency they need without type-asserting to reach real fields.

// the consumer declares exactly what it needs; the concrete store needn't know
type UserFetcher interface {
    GetUser(id string) (*User, error)
}

type Processor struct{ fetcher UserFetcher }

In this repo: the deliberate interface seams are TaskHandler / StreamableTaskHandler, LLMClient, Storage, and the artifact storage interfaces - these exist precisely because they get swapped (custom handlers, in-memory vs Redis storage, filesystem vs MinIO artifacts) and mocked. Add a new interface only when it earns a real second implementation or a generated mock; keep it small.

Errors are values

Check them explicitly; they aren't exceptions. Wrap with %w to add context while preserving the chain for errors.Is / errors.As. Combine siblings with errors.Join (Go 1.20) - no multierr needed.

data, err := os.ReadFile(path)
if err != nil {
    return fmt.Errorf("loading config %s: %w", path, err)
}

Functional options for complex construction

When a type has many optional settings, avoid telescoping constructors:

type Option func(*Server)

func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } }

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

In this repo: construction goes through the fluent builders (A2AServerBuilder, AgentBuilder, ArtifactsServerBuilder) with WithXxx(...) methods - the same "sane defaults, override as needed" philosophy. Extend a builder with a new WithXxx rather than adding a positional constructor argument.

Testing is just Go

Go testing is just Go programming - reach for the stdlib testing package, not a BDD framework.

  • Table-driven tests with t.Run subtests are the standard.
  • Call t.Helper() in assertion helpers so failures point at the case, not the helper.
  • Prefer simple fakes/stubs for stand-ins - implicit interfaces make them cheap to hand-write.
  • Put fixtures in testdata/ (the tool ignores it); diff large output against golden files.
  • Compare structs with github.com/google/go-cmp/cmp, not reflect.DeepEqual.
func TestParse(t *testing.T) {
    tests := []struct {
        name    string
        in      string
        wantErr bool
    }{
        {"valid", "port=8080", false},
        {"bad", "port=abc", true},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            _, err := Parse(tt.in)
            if (err != nil) != tt.wantErr {
                t.Fatalf("Parse(%q) err = %v, wantErr %v", tt.in, err, tt.wantErr)
            }
        })
    }
}

In this repo: fakes for the domain interfaces are counterfeiter-generated under client/mocks/ and server/mocks/, not hand-written. Regenerate with task generate:mocks (the pre-commit hook runs it). For a new interface, add its generate:mock:* target in Taskfile.yml (each points at a specific source file via sources:). Use the generated fakes - don't hand-roll rivals. Table tests, testdata, and go-cmp still apply.

Use current Go, not 2018 Go

This repo targets Go 1.26 - reach for the modern stdlib before any third-party helper or hand-rolled loop:

Instead ofUse
sort.Slice(s, ...)slices.Sort(s), slices.SortFunc, slices.Contains/Index
manual key/value loopsmaps.Keys/Values/Clone/Copy/Equal
if x == 0 { x = def }cmp.Or(x, def); min/max built-ins
ad-hoc multi-error joinserrors.Join(err1, err2)
atomic.AddInt64(&n, 1)typed atomic.Int64/Bool/Pointer[T] (n.Add(1))
gorilla/mux for basic routingnet/http: mux.HandleFunc("GET /u/{id}", ...) + r.PathValue("id")
for i := 0; i < n; i++for i := range n
interface{}any
// +build tags//go:build

To keep a request's values but drop its cancellation for work that outlives it, use context.WithoutCancel(ctx) (Go 1.21).

Generics eliminate duplication, not model hierarchies

Use a generic when the same algorithm runs over many concrete types.

func Map[S, T any](s []S, f func(S) T) []T {
    out := make([]T, len(s))
    for i, v := range s {
        out[i] = f(v)
    }
    return out
}

Do not write generic "repositories", services, or base types - that's Java in Go syntax. Use comparable for map keys/equality and cmp.Ordered for </>. Start concrete; generify only once the same logic repeats across 3+ types.

Logging discipline

logger.Debug("cache miss", "key", key)      // high-volume internal state
logger.Info("server started", "addr", addr) // lifecycle events
logger.Warn("retrying", "attempt", n)        // recoverable problems
logger.Error("request failed", "err", err)   // needs attention
  • Inject the logger as a dependency; no package-level logger beyond main.
  • Never log and return the same error - log once at the boundary, return it up the stack everywhere else.

In this repo: logging is go.uber.org/zap (*zap.Logger, injected into the agent/server/handlers), not slog. The discipline above is identical

  • use zap.String/zap.Error typed fields.

Concurrency, briefly

  • Share memory by communicating - pass data over a channel instead of guarding it with a mutex where you can. Channels orchestrate; mutexes serialize.
  • Bound concurrency with a buffered-channel semaphore, not a static worker pool (Go's scheduler is cheap):
func FetchAll(ctx context.Context, urls []string, max int) error {
    sem := make(chan struct{}, max)
    g, ctx := errgroup.WithContext(ctx)
    for _, u := range urls {
        sem <- struct{}{} // blocks at the limit
        g.Go(func() error {
            defer func() { <-sem }()
            return fetch(ctx, u)
        })
    }
    return g.Wait()
}
  • Never start a goroutine without knowing how it stops - every go func() needs a ctx/closed-channel exit, or it leaks.

For channel patterns in depth - done-channel, fan-out/fan-in, pipeline, or-done, context propagation, worker semaphore - see the go-concurrency skill, and verify with go test -race.

Debugging: the Go toolchain is not the bug

go build/test/run are deterministic and the build cache is keyed by source - don't suspect them or reach for go clean -cache. When an error survives your edit, it's almost always one of these, in order:

  1. The edit didn't fix the actual logic.
  2. It was in the wrong file, function, or package.
  3. A second call site has the same bug, unfixed.
  4. The error comes from a different code path than you edited.

Re-read the (accurate) error, confirm the file is actually compiled (go list -f '{{.GoFiles}}' .), and add a t.Log/print at the exact site.


Adapted from spf13/go-skills (MIT, by Steve Francia), trimmed and reconciled with this repo's conventions. Pairs with go-concurrency and go-spec-reviewer.

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Idiomatic Go concurrency - sync primitives, channel semantics, the select statement, and the standard channel patterns (cancellation/done-channel, fan-out/fan-in, pipeline, or-done, context, worker-pool). Use when writing or reviewing Go that spawns goroutines, shares state, or coordinates over channels, even if the user never says "concurrency" - e.g. data races, deadlocks, goroutine leaks, sync/context/select usage, or "make this parallel". Distilled from luk4z7/go-concurrency-guide.

The English summary is being prepared. Showing the original description.

inference-gateway/adk25Updated Oct 15, 2026

Review a Go design spec before implementation begins - dispatch a subagent that checks a design doc for completeness, consistency, and idiomatic Go (simplicity, small consumer-defined interfaces, explicit wrapped errors, context propagation). Use when a user has a Go spec to review, asks "is this spec ready?", or is about to implement from a written design. Distilled from spf13/go-skills and adapted to this repo's conventions.

The English summary is being prepared. Showing the original description.

inference-gateway/adk25Updated Oct 15, 2026

See all skills by inference-gateway

Report a problem with this skill