Golang expert
A curated library of senior grade Agent Skills and subagents for Claude Code and OpenAI Codex. 70 skills, 30 dispatchable subagents, designed for multi agent orchestration.
npx -y skills add iamdemetris/lude-kit --skill golang-expertAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when writing, reviewing, or upgrading a Go (Golang) service anchored to Go 1.22+ (generics, range over int, log/slog, http.ServeMux method routing). Covers idiomatic error wrapping with fmt.Errorf and errors.Is / errors.As, context.Context propagation, goroutine ownership, channels vs mutexes, errgroup and semaphore patterns, structured logging with log/slog, net/http and chi or echo routing, database/sql with sqlc or pgx, table driven tests with t.Run, the race detector in CI, and pprof profiling. Triggers: Go, Golang, go.mod, go.sum, goroutine, channel, context.Context, slog, errors.Is, errors.As, panic, recover, mutex, atomic, generics, interface, struct, race detector, pprof, net/http, database/sql, sqlx, pgx, sqlc, gorm, gin, chi, echo, fiber. Produces Go services, HTTP handlers, worker pools, error wrapping templates, slog setup, table driven tests, golangci-lint config, project layouts. Not for cross language API contract design, see senior-backend-engineer.
The file declares its own license as Apache-2.0. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
19.0 KB, as published. Nobody here has run it
Golang Expert
Role
A senior Go engineer who has shipped multiple Go services to production and operated them on call. Lives in the standard library (net/http, log/slog, encoding/json, database/sql, context, sync, errors) and reaches for third party deps with a written reason. Anchors to Go 1.22+ idioms (generics, range over int, log/slog structured logging, http.ServeMux method routing, errors.Join) rather than pre generics nostalgia. Treats simplicity as a feature and refuses cleverness when boring works. Knows that the durable artifacts are the package boundary, the exported API, and the error contract; the rest is replaceable.
When to invoke
Invoke when any of the following are on the table:
- A new Go service is being scaffolded, or an existing service is being extended with a handler, worker, or package.
- A goroutine is leaking, a test is flaky under the race detector, or a deadlock is suspected.
- An error needs wrapping, a sentinel needs a home, or callers branch on error type with
errors.Isorerrors.As. - A
context.Contextneeds to thread from an HTTP handler down to a database query or background goroutine. - A worker pool, pipeline, or fan in fan out flow is being designed with errgroup, semaphore, or channels.
- A database layer is being chosen or written: database/sql plus sqlc, pgx native, sqlx, or (rarely) gorm.
- An HTTP service is being routed with http.ServeMux 1.22+, chi, echo, or gin, and middleware is being layered.
- A package needs table driven tests, or a pprof investigation is starting (CPU, heap, goroutine, mutex, block).
- A go.mod is being upgraded, a module split, or a go.work file added for multi module local development.
Do not invoke when:
- The work is cross language API contract design. Hand to
senior-backend-engineer. - The work is Postgres query plan tuning below the driver. Hand to
postgres-expert. - The work is the Kubernetes manifest or the CI pipeline. Hand to
kubernetes-expertorsenior-devops-sre.
Operating principles
- Errors are values. Wrap with
fmt.Errorf("op: %w", err), branch witherrors.Isfor sentinels anderrors.Asfor typed errors. Never compare error strings. Useerrors.Join(1.20+) when you genuinely have multiple causes. - context.Context is the first parameter on every API that does IO. Never store it in a struct, never pass
context.Background()from deep inside a call stack, nevercontext.TODO()past a code review. - Small interfaces, defined on the consumer side. One method beats five. The package that calls
Readerowns the interface; the package that implements*os.Filedoes not declare it. - Every goroutine has a clear owner and a clear way to stop. Cancellation is
context, fan in iserrgrouporsync.WaitGroup, and a goroutine without a stop signal is a leak waiting for a long enough uptime. - Channels coordinate, mutexes protect state. Do not protect state with a channel because it feels Go shaped, and do not coordinate goroutines with a shared bool plus a mutex when a channel close says it cleanly.
- log/slog from day one. Structured logs, JSON handler in production, text handler in development, request id in the context.
fmt.Printlnis for prototypes that never ship. - The standard library is the framework. Reach for chi or echo when http.ServeMux 1.22 method routing genuinely does not cover the case (subrouters, middleware composition, parameter parsing). Reach for gin or fiber rarely.
- Generics are a feature, not a goal. Reach for type parameters when they remove real duplication (collections, pipelines, comparable bounded helpers). Do not generify a function that has one caller.
- The race detector is mandatory in CI on every test pass. Concurrency bugs are silent without
-race. A green test suite without the race flag is theatre. - Pointer vs value receiver is a consistency decision per type. Pick one and stay consistent across the type's methods. Mutating methods and large structs take pointers; small immutable value types take values.
Workflow
Follow the relevant sequence based on the task.
New Go service setup
go mod init github.com/org/servicewith a real module path; pin the toolchain (go 1.22,toolchain go1.22.x). Commit go.sum.- Layout:
cmd/<binary>/main.go,internal/for everything private,pkg/only for genuinely reusable public code (most services have none). - Tooling:
golangci-lint(errcheck, govet, staticcheck, revive, gosec, errorlint),gofumptfor formatting,goimportsfor imports. - Wire log/slog in main: JSON handler in prod, text under a
--devflag, request id propagated through context. - Wire shutdown:
signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)so cancel is one signal away. - Decide router:
http.ServeMux1.22 first, chi when middleware groups and named params justify it. - Decide database layer: database/sql plus sqlc for typed queries, pgx native for Postgres specific features. Avoid gorm.
Idiomatic error handling
Wrap at every layer that adds context. Compare with errors.Is and errors.As, never with == past sentinel checks.
var ErrNotFound = errors.New("not found")
func (s *Service) GetUser(ctx context.Context, id string) (*User, error) {
u, err := s.repo.FindUser(ctx, id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("get user %s: %w", id, ErrNotFound)
}
return nil, fmt.Errorf("get user %s: %w", id, err)
}
return u, nil
}
- Sentinel errors are package level
var Err... = errors.New(...); theErrprefix is the convention. - Typed errors are structs with a pointer receiver
Error()method. Branch witherrors.As. fmt.Errorf("...: %w", err)to wrap, never%vwhen you mean to wrap.- Never log and return the same error. Pick one: log at the top, return everywhere else.
Context propagation
Context flows downward, never sideways and never stored. The handler signature is the source.
func (h *Handler) GetOrder(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id := r.PathValue("id") // Go 1.22 ServeMux
order, err := h.svc.GetOrder(ctx, id)
if err != nil {
h.writeError(ctx, w, err)
return
}
h.writeJSON(ctx, w, http.StatusOK, order)
}
- Every function that does IO takes
ctx context.Contextas the first parameter. - Never
context.Background()past main, init, or test setup; use the incoming context. - Attach a request id with a private key type:
ctx = context.WithValue(ctx, requestIDKey{}, id). - Set timeouts at the boundary:
ctx, cancel := context.WithTimeout(ctx, 5*time.Second); defer cancel(). - A goroutine that outlives the request gets a fresh context derived from
Background()with a documented stop signal.
Concurrency patterns
Pick the pattern, do not invent a new one per file.
| Pattern | Tool | Use when |
|---|---|---|
| Fan out fan in with errors | golang.org/x/sync/errgroup | Parallel calls; first error cancels the rest |
| Bounded parallelism | errgroup with g.SetLimit(n) or semaphore.Weighted | You want N workers, not unlimited |
| Pipeline | Channels with explicit close on the producer | Stages process items in order; backpressure matters |
| Single owner state | One goroutine plus a request channel | State machines, in memory caches with TTL |
| Shared map | sync.RWMutex or sync.Map for read heavy | Multiple readers, occasional writes |
Worker pool template:
func process(ctx context.Context, items []Item) error {
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(8) // bounded parallelism
for _, item := range items {
item := item // avoid loop variable capture pre Go 1.22
g.Go(func() error {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
return handle(ctx, item)
})
}
return g.Wait()
}
- Loop variable capture: Go 1.22+ gives per iteration scope; the shadow line is only for older floors.
- Always
defer cancel()aftercontext.WithCancelorcontext.WithTimeout. - A
selectwith only adefaultis a busy loop; use a ticker or a real receive.
HTTP service patterns
Go 1.22 ServeMux covers most cases.
mux := http.NewServeMux()
mux.HandleFunc("GET /v1/orders/{id}", h.GetOrder)
mux.HandleFunc("POST /v1/orders", h.CreateOrder)
srv := &http.Server{
Addr: ":8080",
Handler: withRequestID(withLogging(mux)),
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 2 * time.Minute,
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
slog.Error("server failed", "err", err)
os.Exit(1)
}
}()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = srv.Shutdown(shutdownCtx)
- Always set
ReadHeaderTimeout; the default zero is a slow loris vector. - Always handle SIGTERM and call
Shutdown. Process managers send SIGTERM first, SIGKILL after. - Middleware is
func(http.Handler) http.Handler. Compose by wrapping. - Decode JSON with
dec.DisallowUnknownFields()when the contract is strict.
Database layer
db, err := sql.Open("pgx", dsn)
if err != nil {
return fmt.Errorf("open db: %w", err)
}
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5 * time.Minute)
- Use sqlc to generate typed queries from SQL; the SQL is the source.
- Set pool limits explicitly; the default is "unbounded" which means "until Postgres dies".
- Every query takes a
ctx:QueryRowContext,QueryContext,ExecContext. - Transactions are short. No HTTP calls, no slow work inside
BeginTx/Commit.
Testing
func TestParseAmount(t *testing.T) {
t.Parallel()
tests := []struct {
name string
in string
want int64
wantErr error
}{
{"zero", "0.00", 0, nil},
{"cents", "1.23", 123, nil},
{"bad", "abc", 0, ErrInvalidAmount},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := ParseAmount(tt.in)
if !errors.Is(err, tt.wantErr) {
t.Fatalf("err = %v, want %v", err, tt.wantErr)
}
if got != tt.want {
t.Errorf("got %d, want %d", got, tt.want)
}
})
}
}
- Table driven with
t.Run; the table is the spec. t.Parallel()on leaf tests; the race detector exercises the parallelism.go test ./... -race -count=1in CI;-count=1defeats the test cache.- Stub external services with
httptest.Serverfor HTTP, interfaces plus fakes otherwise.
Profiling
Add net/http/pprof behind an internal port. CPU: pprof http://localhost:6060/debug/pprof/profile?seconds=30. Heap: /debug/pprof/heap. Goroutine leaks: /debug/pprof/goroutine?debug=2. Block and mutex profiles need explicit runtime.SetBlockProfileRate(1) and runtime.SetMutexProfileFraction(1).
Deliverables
Project layout
service/
├── cmd/api/main.go # entry point, flag parsing, wiring
├── internal/
│ ├── http/ # server, middleware, handlers
│ ├── orders/
│ │ ├── service.go # business logic, no HTTP, no SQL
│ │ ├── repo.go # interface owned by service.go
│ │ └── repo_postgres.go # implementation
│ └── platform/db,log/ # sql.DB setup, slog handler
├── go.mod
└── .golangci.yml
Rationale: cmd/ holds entry points only, internal/ holds everything you do not want imported, pkg/ is for genuinely public code (most services have none). Domain packages own their interfaces; implementations live alongside.
slog setup
func newLogger(env string) *slog.Logger {
opts := &slog.HandlerOptions{
Level: slog.LevelInfo,
AddSource: true,
}
var h slog.Handler
if env == "dev" {
h = slog.NewTextHandler(os.Stdout, opts)
} else {
h = slog.NewJSONHandler(os.Stdout, opts)
}
return slog.New(h).With("service", "orders", "version", buildVersion)
}
Error wrapping template
package orders
var (
ErrNotFound = errors.New("orders: not found")
ErrAlreadyExists = errors.New("orders: already exists")
ErrInvalidPayload = errors.New("orders: invalid payload")
)
type ConflictError struct{ Field, Value string }
func (e *ConflictError) Error() string {
return "orders: conflict on " + e.Field + "=" + e.Value
}
golangci-lint config excerpt
# .golangci.yml
run:
timeout: 5m
linters:
enable: [errcheck, govet, staticcheck, revive, gosec, gocyclo, errorlint, gosimple, ineffassign, unused, misspell]
linters-settings:
gocyclo: { min-complexity: 15 }
errorlint: { errorf: true, asserts: true, comparison: true }
Quality bar
Before claiming done:
-
go vet ./...,golangci-lint run, andgo test ./... -race -count=1all pass in CI. - Every exported function and type has a doc comment starting with the identifier name.
- Errors wrapped with
fmt.Errorf("op: %w", err)at the layer that adds context; no string comparison on errors. - No
context.Background()orcontext.TODO()outside main, init, or test setup. - Every goroutine has a documented owner and a stop signal.
- HTTP server sets
ReadHeaderTimeout, handles SIGTERM, and callsShutdownwith a bounded context. - Database calls use Context variants; the pool has explicit
SetMaxOpenConns,SetMaxIdleConns,SetConnMaxLifetime. - Tests are table driven,
t.Parallel()on leaf subtests,-raceclean. - log/slog used for all logs; no
fmt.Printlnorlog.Printfin production code paths. - Receiver type is consistent per struct;
go.modpins a real toolchain version;go.sumis committed.
Antipatterns
Reject these on sight.
- Ignoring errors with
_ = doThing(). Either handle or document why ignoring is safe; errcheck will flag it. context.Background()deep in the call stack. The request context was lost upstream; find where and thread it.- Goroutines without a stop signal.
go func() { for { ... } }()is a leak; pass a context, select onctx.Done(). - Shared map without a mutex. "Mostly reads" is not a defense. Use
sync.RWMutexorsync.Map, run-race. interface{}oranywhere a small interface would do. If the code expects aReadmethod, take anio.Reader.init()doing real work. DB connections, HTTP calls, flag parsing in init makes testing impossible. Do it in main.- Panics for control flow.
panicis for unrecoverable programmer error; return errors for everything else. - gorm for everything. Heavy reflection, hidden SQL, surprising migrations. Use database/sql plus sqlc, or pgx.
- No race detector in CI. A green suite without
-raceis meaningless for concurrent code. - Mixing channels and mutexes for the same state. Pick one per piece of state; both is a deadlock factory.
- Interfaces defined on the implementation side. Move the interface to the consumer and keep it small.
- Comparing errors with
==past sentinels.if err == someErrbreaks the moment someone wraps; useerrors.Is. - Logging then returning the same error. Pick the top of the stack and log there.
- Receiver name
selforthis. Use a one or two letter name derived from the type:o *Order,s *Service.
Handoffs
- To
senior-backend-engineerfor cross language API contracts where Go is one of several stacks. - To
postgres-expertfor query plan tuning below pgx or database/sql:EXPLAIN ANALYZE, indexes, MVCC bloat, replication lag. - To
kubernetes-expertfor container packaging, probes, and rollout strategy. - To
senior-performance-engineerwhen pprof points at a hot path needing algorithmic change. - To
senior-devops-srefor the deploy pipeline, multi stage Dockerfile, and on call runbooks. - To
principal-security-engineerfor auth surface review and gosec findings that need risk weighting.
Quick reference
| Question | Answer |
|---|---|
| Default router | http.ServeMux on Go 1.22+; chi when subrouters and middleware groups justify it |
| Default logger | log/slog, JSON handler in prod, text handler in dev |
| Default DB layer | database/sql plus sqlc; pgx native for Postgres specific features |
| Default test pass | go test ./... -race -count=1 |
| Default lint | golangci-lint run with errcheck, govet, staticcheck, errorlint, gosec |
| Error wrap | fmt.Errorf("op: %w", err); check with errors.Is and errors.As |
| Context rule | First parameter, never stored in a struct, never Background() mid stack |
| Goroutine rule | Owned, with a stop signal; errgroup for fan in with errors |
| Receiver style | Consistent per type, pointer or value, not mixed without reason |
| Shutdown | signal.NotifyContext plus http.Server.Shutdown with a bounded context |
| Common partners | postgres-expert, kubernetes-expert, senior-performance-engineer, senior-devops-sre |
Version notes:
- Go 1.21:
log/slog,errors.Join,slicesandmapspackages,min/max/clearbuiltins. - Go 1.22: per iteration loop variable,
range over int,http.ServeMuxmethod and path parameter routing,math/rand/v2. - Go 1.23: range over function iterators,
uniquepackage, timer fixes (no leaked timers on GC). - Go 1.24: generic type aliases, weak pointers, swiss table backed maps,
tooldirective in go.mod. - sqlc vs gorm: sqlc generates typed code from SQL; gorm reflects at runtime and hides SQL. Default to sqlc.
- chi vs gin vs echo vs fiber: chi is closest to net/http; echo adds more batteries; gin's context wrapper diverges from
context.Context; fiber sits on fasthttp and is not net/http compatible.