From go-standards
Load the full Go service architecture standards for cross-layer review or greenfield implementation
How this skill is triggered — by the user, by Claude, or both
Slash command
/go-standards:go-standardsThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Load the full Go service architecture standards. Use this for greenfield service implementation or cross-layer review.
Load the full Go service architecture standards. Use this for greenfield service implementation or cross-layer review.
This document describes the top-level structure for a service in this project. Before building any layer, read this file. Then read the spec file for the specific layer you are implementing.
Every service follows this layout:
services/<name>/
├── main.go # Env validation, dependency wiring, server start
├── handler/
│ └── handler.go # Transport layer
├── service/
│ └── service.go # Business logic
├── database/
│ └── database.go # Data access
├── client/
│ └── client.go # Outbound clients to other services or APIs
├── types/
│ └── types.go # Local interfaces for every dependency
└── Dockerfile
Each directory is its own Go package named after the directory.
Implement layers in this order. Each layer depends on the one before it.
types/ — define all interfaces first so every other layer has a contract to depend ondatabase/ — implement the data access layer against the Database interfaceclient/ — implement any outbound clients against their interfacesservice/ — implement business logic using only the interfaces from types/handler/ — implement the transport layer using only the Service interface from types/main.go — wire all concrete implementations together and start the serverDepend on interfaces, not implementations. Every layer holds its dependencies as interfaces from types/. Nothing is imported directly from sibling packages except in main.go.
Inject all dependencies through constructors. All dependencies are passed into New* functions. Nothing is instantiated inside the service or handler.
One responsibility per layer. Transport logic does not touch the database. Business logic does not build queries. Queries do not make outbound calls.
Crash on bad config, log on recoverable errors. Missing environment variables are fatal at startup. Cache failures and best-effort side effects are logged and swallowed. Anything that prevents a correct response is returned as an error.
Shared infrastructure lives in services/common. Reusable clients, interfaces, and domain models are defined there and imported. Never copy infrastructure code into a service.
types/types.goDefine the interfaces that every other layer in the service depends on. This file is the contract layer — it describes what each dependency must provide without specifying how.
Service, Database, SomeClient, etc.Service interface must exactly match the public methods implemented on the service struct — the handler depends on itservices/common and are imported here as neededhandler, service, database, client) — only from services/common and standard librariespackage types
import (
"context"
commontypes "github.com/kabradshaw1/story/services/common"
"github.com/kabradshaw1/story/services/common/genproto/<name>"
)
type Service interface {
MethodOne(ctx context.Context, req *proto.Request) (*proto.Response, error)
MethodTwo(ctx context.Context, req *proto.Request) (*proto.Response, error)
}
type Database interface {
FindRecord(ctx context.Context, id int) (*commontypes.SomeModel, error)
SaveRecord(ctx context.Context, record commontypes.SomeModel) error
}
type SomeClient interface {
Notify(ctx context.Context, payload SomePayload) error
}
Service interface matches the service struct's public methods exactlyservices/common, not redefineddatabase/database.goExecute all data access operations. This is the only layer that communicates with the database.
Connect function (or equivalent) accepts a connection string, establishes the connection, and returns a typed *DB struct; it calls log.Fatal on failurecontext.Context as its first argument and passes it through to the underlying querypackage database
type DB struct {
client *SomeClient
}
func Connect(connectionString string) *DB {
// connect, fatal on error
return &DB{client: client}
}
func (d *DB) FindRecord(ctx context.Context, id int) (*commontypes.SomeModel, error) {
// query using ctx
}
func (d *DB) SaveRecord(ctx context.Context, record commontypes.SomeModel) error {
// insert using ctx
}
For any mutation scoped to a user, always verify ownership before modifying data:
record.User == requestingUser — return a descriptive error if notAfter an update query, do not re-fetch the record to return it. Instead, update the already-fetched struct in memory:
// fetch → verify → update → return modified struct
record.Field = newValue
return &record, nil
fmt.Errorf("failed to find record: %w", err)Connect accepts a connection string and calls log.Fatal on failurecontext.Context as its first parameterclient/client.goHandle all outbound calls to external services or third-party APIs. This is the only layer that makes outbound network requests.
New* constructor initializes and returns the client structcontext.Context as its first argument and passes it to the underlying callpackage client
type SomeClient struct {
// underlying transport (http.Client, grpc.ClientConn, etc.)
}
func New() *SomeClient {
return &SomeClient{
// initialize transport
}
}
func (c *SomeClient) Notify(ctx context.Context, payload SomePayload) error {
if payload.RequiredField == "" {
return fmt.Errorf("RequiredField is required")
}
// construct and send request using ctx
// check response and return error on failure
return nil
}
New* constructor initializes the transportcontext.Contextservice/service.goImplement all business logic. Orchestrate calls to the database, cache, and outbound clients to fulfill a request.
types/ — never concrete implementationsNew* constructor accepts every dependency as an argument — nothing is instantiated insidetypes.Database methods onlypackage service
type Service struct {
db types.Database
cache commontypes.Cache
client types.SomeClient
}
func NewService(db types.Database, cache commontypes.Cache, client types.SomeClient) *Service {
return &Service{db: db, cache: cache, client: client}
}
Use a read-through cache on any method that reads data. Keys must use : as a separator between all components to prevent collisions.
cacheKey := "resource:" + userID + ":" + entityType
Order of operations:
Cache errors (get, set, unmarshal, marshal) are always logged and swallowed — a cache failure must never prevent a valid response from being returned.
cacheData, err := s.cache.Get(ctx, cacheKey)
if err == nil && cacheData != "" {
if err := json.Unmarshal([]byte(cacheData), &response); err != nil {
log.Printf("<service>/service: Method: failed to unmarshal cache: %v", err)
} else {
return &response, nil
}
}
// ... query db, build response ...
serializedData, err := json.Marshal(&response)
if err != nil {
log.Printf("<service>/service: Method: failed to marshal for cache: %v", err)
} else {
if err := s.cache.Set(ctx, cacheKey, string(serializedData), 10*time.Minute); err != nil {
log.Printf("<service>/service: Method: failed to set cache: %v", err)
}
}
fmt.Errorf("description: %w", err)All log messages must identify the service and method:
"<service>/service: MethodName: description of what failed"
types.* interfaces and commontypes.* interfaces — no concrete typesNew* constructor accepts all dependencies as arguments: separators between all variable componentsfmt.Errorf<service>/service: Method: description formathandler/handler.goReceive inbound requests and delegate to the service. This is the only layer that knows about the transport mechanism (HTTP, gRPC, queue consumer, webhook, etc.).
types.Service interface — never a concrete *service.ServiceNew* constructor accepts types.Service and registers or returns the handlerpackage handler
type Handler struct {
svc types.Service
}
func New(svc types.Service) *Handler {
return &Handler{svc: svc}
}
func (h *Handler) MethodOne(ctx context.Context, req *Request) (*Response, error) {
return h.svc.MethodOne(ctx, req)
}
types.Service, not *service.ServiceNew* constructor accepts types.Serviceservice/, database/, or client/Write tests for every layer and run them before considering any implementation complete. Do not skip or comment out failing tests — fix the underlying issue.
Unit tests use mocks for all dependencies. They require no running infrastructure and should always pass locally.
File placement: alongside the file being tested, in the same package.
handler/handler_test.go
service/service_test.go
database/database_test.go
client/client_test.go
Run with:
go test ./...
service/service_test.go — highest priority
For each service method:
handler/handler_test.go
For each handler method:
database/database_test.go
For each database method:
client/client_test.go
For each client method:
Because every dependency is an interface from types/, mocks can be written by hand or generated. A mock must:
Integration tests verify that two or more real components work correctly together. No mocks — real infrastructure only.
File placement:
integration/integration_test.go
What to cover:
Run with:
go test ./integration/... -tags integration
Only run when the required infrastructure is available (local Docker, CI services, etc.).
End-to-end tests start the full service — real transport, database, and cache — and exercise it as a client would.
File placement:
e2e/e2e_test.go
What to cover:
Setup: the e2e test starts the server with a real test configuration and tears it down after the suite. Seed required data before tests run and clean it up afterward.
Run with:
go test ./e2e/... -tags e2e
Run in CI on pull requests and before releases.
| Type | Mocks | Infrastructure needed | When to run |
|---|---|---|---|
| Unit | All dependencies | None | Always |
| Integration | None | Test DB / server | When infrastructure is available |
| End-to-end | None | Full service stack | CI / pre-release |
npx claudepluginhub kabradshaw1/go-standards --plugin go-standardsProvides idiomatic Go patterns for backend APIs with Gin, Echo, Fiber: standard project structure, custom error handling, handler dependency injection, concurrency best practices.
Writes idiomatic Go code, reviews PRs, debugs tests, designs APIs, and applies security patterns. Covers table-driven tests, error wrapping, goroutines, generics, gRPC with Google AIP, golangci-lint, and slog logging.