pedantic-coder/skills/go-pedantry/SKILL.md
This skill should be used when the user is writing Go code and needs guidance on Go-specific pedantry: error wrapping with fmt.Errorf and %w, interface design (accept interfaces return structs), package naming conventions, struct field ordering, receiver naming, golangci-lint configuration, and Go-specific patterns that go beyond universal principles.
npx skillsauth add oborchers/fractional-cto go-pedantryInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Go is a language that already has strong opinions: gofmt, short variable names, explicit error handling, no exceptions. But the language's opinions stop at syntax. How you wrap errors, design interfaces, name packages, order struct fields, and configure your linter -- these are decisions Go leaves to you. This skill makes those decisions for you, because inconsistency in these areas is what turns a Go codebase from clean to chaotic.
%w, Never %s or %vWhen wrapping an error with fmt.Errorf, ALWAYS use %w. This preserves the error chain so that errors.Is() and errors.As() work. Using %s or %v converts the error to a string and destroys the chain. This is not a style preference -- it is the difference between errors that can be programmatically handled and errors that can only be logged.
// BAD -- error chain is destroyed, errors.Is() will not work
func GetUser(ctx context.Context, id string) (*User, error) {
user, err := db.QueryUser(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to get user %s: %s", id, err.Error())
}
return user, nil
}
// BAD -- %v also destroys the chain
func GetUser(ctx context.Context, id string) (*User, error) {
user, err := db.QueryUser(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to get user %s: %v", id, err)
}
return user, nil
}
// GOOD -- %w preserves the error chain
func GetUser(ctx context.Context, id string) (*User, error) {
user, err := db.QueryUser(ctx, id)
if err != nil {
return nil, fmt.Errorf("getting user %s: %w", id, err)
}
return user, nil
}
// Now callers can inspect the error chain:
user, err := GetUser(ctx, "usr_123")
if errors.Is(err, sql.ErrNoRows) {
// handle not found
}
Error message conventions:
"getting user %s: %w" not "an error occurred while attempting to retrieve the user with ID %s from the database: %w"Sentinel errors (package-level error values) follow a strict naming convention:
// GOOD -- exported, Err prefix, PascalCase after prefix
var (
ErrNotFound = errors.New("not found")
ErrAlreadyExists = errors.New("already exists")
ErrInvalidInput = errors.New("invalid input")
ErrUnauthorized = errors.New("unauthorized")
ErrRateLimited = errors.New("rate limited")
)
// BAD -- every one of these violates the convention
var (
NotFoundError = errors.New("not found") // wrong: no Err prefix
errnotfound = errors.New("not found") // wrong: unexported + no camelCase
NOT_FOUND = errors.New("not found") // wrong: not Go convention
UserNotFound = errors.New("user not found") // wrong: no Err prefix
)
Custom error types follow the same prefix pattern:
// GOOD
type ErrValidation struct {
Field string
Message string
}
func (e *ErrValidation) Error() string {
return fmt.Sprintf("validation error on %s: %s", e.Field, e.Message)
}
Functions accept interfaces so they can work with any implementation. Functions return concrete structs so the caller has the full type. This is the fundamental Go interface rule.
// BAD -- returning an interface hides the concrete type
func NewUserService(db Database) UserServiceInterface {
return &UserService{db: db}
}
// BAD -- accepting a concrete type prevents testing and alternative implementations
func ProcessOrder(service *StripeService, order *Order) error {
return service.Charge(order.Total)
}
// GOOD -- accept interface, return struct
func NewUserService(db UserRepository) *UserService {
return &UserService{db: db}
}
// GOOD -- accept interface for the dependency
func ProcessOrder(payment PaymentProcessor, order *Order) error {
return payment.Charge(order.Total)
}
Go interfaces should be small. One to three methods. If your interface has more than three methods, it is probably doing too much.
// BAD -- too many methods, too specific
type UserManager interface {
GetUser(ctx context.Context, id string) (*User, error)
CreateUser(ctx context.Context, input CreateUserInput) (*User, error)
UpdateUser(ctx context.Context, id string, input UpdateUserInput) (*User, error)
DeleteUser(ctx context.Context, id string) error
ListUsers(ctx context.Context, opts ListOptions) ([]*User, error)
SearchUsers(ctx context.Context, query string) ([]*User, error)
CountUsers(ctx context.Context) (int, error)
}
// GOOD -- small, focused interfaces
type UserReader interface {
GetUser(ctx context.Context, id string) (*User, error)
}
type UserWriter interface {
CreateUser(ctx context.Context, input CreateUserInput) (*User, error)
UpdateUser(ctx context.Context, id string, input UpdateUserInput) (*User, error)
DeleteUser(ctx context.Context, id string) error
}
type UserLister interface {
ListUsers(ctx context.Context, opts ListOptions) ([]*User, error)
}
Interfaces belong where they are used, not where they are implemented. The consumer defines what it needs; the implementation satisfies it without knowing.
// BAD -- interface defined in the implementation package
// package database
type UserStore interface {
Get(ctx context.Context, id string) (*User, error)
Save(ctx context.Context, user *User) error
}
type PostgresUserStore struct { ... }
// GOOD -- interface defined where it is used
// package handler (the consumer)
type UserGetter interface {
Get(ctx context.Context, id string) (*User, error)
}
type GetUserHandler struct {
users UserGetter // accepts interface
}
// package database (the implementation)
type PostgresUserStore struct { ... }
func (s *PostgresUserStore) Get(ctx context.Context, id string) (*User, error) { ... }
// PostgresUserStore satisfies handler.UserGetter without importing it
Single-method interfaces are named: Verb + "er".
| Method | Interface Name |
|--------|---------------|
| Read() | Reader |
| Write() | Writer |
| Close() | Closer |
| Format() | Formatter |
| Validate() | Validator |
| Handle() | Handler |
| Encode() | Encoder |
Go packages have strict naming conventions that the community enforces by reputation.
Rules:
user, auth, config, orderuserservice not userService or user_serviceuser.Service not user.UserServiceutils, helpers, common, misc, shared, base -- these are organizational failures// BAD -- package naming violations
package userService // camelCase
package user_service // underscores
package utils // meaningless
package common // meaningless
package helpers // meaningless
// In user package:
type UserService struct{} // stutters: user.UserService
type UserRepository struct{} // stutters: user.UserRepository
// GOOD -- clean package names
package user
type Service struct{} // user.Service -- reads naturally
type Repository struct{} // user.Repository -- no stutter
package auth
type Token struct{} // auth.Token
type Middleware struct{} // auth.Middleware
package order
type Processor struct{} // order.Processor
Struct fields are ordered by purpose, not alphabetically. Exported fields come first. Related fields are grouped. Groups are separated by blank lines.
// BAD -- no logical grouping
type Server struct {
addr string
logger *slog.Logger
TLS bool
db *sql.DB
Port int
Host string
readTimeout time.Duration
cache *Cache
WriteTimeout time.Duration
maxConns int
}
// GOOD -- grouped by purpose, exported first within groups, tags consistent
type Server struct {
// Configuration (exported)
Host string `json:"host" yaml:"host"`
Port int `json:"port" yaml:"port"`
TLS bool `json:"tls" yaml:"tls"`
ReadTimeout time.Duration `json:"read_timeout" yaml:"read_timeout"`
WriteTimeout time.Duration `json:"write_timeout" yaml:"write_timeout"`
MaxConns int `json:"max_conns" yaml:"max_conns"`
// Dependencies (unexported)
db *sql.DB
cache *Cache
logger *slog.Logger
}
Rules:
json:"snake_case")Receivers are one or two lowercase letters derived from the type name. They are consistent across ALL methods on the type. Never use this or self.
// BAD -- inconsistent receivers, verbose names
func (server *Server) Start() error { ... }
func (s *Server) Stop() error { ... }
func (this *Server) Addr() string { ... }
func (self *Server) IsRunning() bool { ... }
// GOOD -- consistent single-letter receiver
func (s *Server) Start() error { ... }
func (s *Server) Stop() error { ... }
func (s *Server) Addr() string { ... }
func (s *Server) IsRunning() bool { ... }
| Type | Receiver |
|------|----------|
| Server | s |
| User | u |
| OrderProcessor | op |
| ConfigService | cs |
| Handler | h |
| Client | c |
| Repository | r |
Two-letter receivers are for when the single letter would be ambiguous (multiple types starting with the same letter in the same file).
context.Context is always the first parameter. Always named ctx. Never stored in a struct.
// BAD -- context in wrong position
func GetUser(id string, ctx context.Context) (*User, error) { ... }
// BAD -- context stored in struct
type Service struct {
ctx context.Context
db *sql.DB
}
// BAD -- context not named ctx
func GetUser(c context.Context, id string) (*User, error) { ... }
// GOOD -- context first, named ctx
func GetUser(ctx context.Context, id string) (*User, error) { ... }
func (s *Service) ProcessOrder(ctx context.Context, order *Order) error { ... }
# .golangci.yml
linters:
enable:
- errcheck # unchecked errors
- gosimple # simplifications
- govet # suspicious constructs
- ineffassign # unused assignments
- staticcheck # advanced static analysis
- unused # unused code
- gocritic # opinionated checks
- revive # flexible linter
- errorlint # error wrapping correctness
- nilerr # returning nil when err is not nil
- wrapcheck # errors from external packages are wrapped
- prealloc # slice pre-allocation
- unconvert # unnecessary type conversions
- tenv # os.Setenv in tests (use t.Setenv)
linters-settings:
gocritic:
enabled-checks:
- appendAssign
- argOrder
- badCall
- badCond
- dupArg
- dupBranchBody
- dupCase
- elseif
- flagDeref
- nilValReturn
- singleCaseSwitch
- underef
- unnecessaryBlock
revive:
rules:
- name: blank-imports
- name: context-as-argument
- name: context-keys-type
- name: dot-imports
- name: error-naming
- name: error-return
- name: error-strings
- name: exported
- name: increment-decrement
- name: indent-error-flow
- name: package-comments
- name: range
- name: receiver-naming
- name: time-naming
- name: var-naming
- name: unexported-return
errorlint:
errorf: true # check fmt.Errorf uses %w
asserts: true # check errors.As uses pointer
comparison: true # check errors.Is instead of ==
Working implementations in examples/:
examples/error-wrapping.md -- Complete Go examples showing proper error wrapping with %w, error message conventions, sentinel errors, and custom error types with errors.Is/errors.As patternsexamples/interface-design.md -- Go examples showing interface design patterns: accept interfaces/return structs, small interfaces, consumer-side definition, and the Verb+er naming conventionWhen reviewing Go code:
fmt.Errorf("context: %w", err) -- never %s or %v for errorsErr prefix with PascalCase: ErrNotFound, ErrInvalidInputReader, Writer, Validator)utils, helpers, commonuser.Service not user.UserServicejson:"snake_case")this or self receiverscontext.Context is always the first parameter, always named ctx, never stored in a structgolangci-lint is configured with at minimum: errcheck, gosimple, govet, staticcheck, unused, gocritic, revive, errorlinttools
This skill should be used when the user invokes any /plan-* command from the planning-tools plugin (/plan-context, /plan-master, /plan-open-questions, /plan-verify, /plan-tick, /plan-progress, /plan-delete), asks how Claude Code's plan files work, asks where plans are stored, asks to author or audit a multi-phase master planning document, asks how to walk through a plan's Open Questions interactively, asks how to write progress entries, or mentions ~/.claude/plans/ or .claude/planning-tools.local.md. Provides the index of planning-tools commands, the master-plan workflow lifecycle, the v0.3.0+ list-shape mandate (phases and questions as headings + bulleted scope items, never tables), the v0.3.2+ plain-bullet shape (no `- [ ]` checkboxes — heading emoji is the sole tick signal), the progress-entry methodology, and the mechanics of Claude Code's plan-mode file storage.
testing
This skill should be used by the plan-verifier agent and the /plan-verify command to audit a drafted master plan against a fixed checklist. Covers universal-core completeness, the v0.3.0+ no-tables-for-phases-or-questions rule, trigger-based section-coverage gaps, phase actionability (heading + per-phase TL;DR + bulleted scope + exit criteria), the v0.3.1+ per-phase TL;DR requirement, the v0.3.2+ plain-bullet scope shape (legacy `- [ ]`/`- [x]` accepted silently), the v0.3.3+ context-block shape (plan-level `**TL;DR:**` + bulleted metadata, legacy `>` blockquote accepted silently), integer phase numbering enforcement, dependency traceability, citation resolution, callout/evidence convention compliance, Open Questions placement, and the one-PR-per-master-plan rule. Single-owner of the audit checklist.
tools
This skill should be used when authoring, reviewing, or modifying a multi-phase master planning document via the planning-tools plugin (especially the /plan-master and /plan-verify commands). Codifies the universal core sections, trigger-based optional sections, integer-only phase numbering, Open Questions placement, one-PR-per-plan rule, status conventions, evidence attribution, callouts, cross-reference formats, the v0.3.0 list-shape mandate (phases and questions are heading + bulleted list, never markdown tables), the v0.3.1 per-phase TL;DR requirement (1–3 sentence what/why summary under each phase heading for glance-ability), the v0.3.2 plain-bullet scope shape (`- <action>` items, no `- [ ]` checkboxes — the phase status emoji is the sole tick signal), and the v0.3.3 context-block shape (a plan-level `**TL;DR:**` + a bulleted metadata list instead of a `>` blockquote; legacy blockquote blocks accepted silently). Project-agnostic — no ticket-prefix or plan-type taxonomy.
testing
This skill should be used when the user is adjusting spacing, padding, margins, content density, section gaps, vertical rhythm, or separation between elements. Also applies when reviewing whether a design feels cramped or too sparse, choosing between borders and whitespace for separation, or defining a spacing system. Covers the 4px/8px spacing system, macro vs micro whitespace, content density spectrum, separation techniques (whitespace > background shifts > borders), and vertical rhythm.