From go-agent-skills
Provides idiomatic Go design patterns like functional options, builder, factory, strategy, middleware chain, pub/sub for structuring types and services. Use for 'design pattern', 'functional options', 'builder pattern', 'how to structure this'.
npx claudepluginhub eduardo-sl/go-agent-skills --plugin go-agent-skillsThis skill uses the workspace's default tool permissions.
Go favors composition over inheritance and simplicity over abstraction.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
Go favors composition over inheritance and simplicity over abstraction. These patterns are idiomatic Go — not Java patterns ported to Go.
The most idiomatic Go pattern for configurable constructors. Use when a type has many optional settings.
type Server struct {
addr string
readTimeout time.Duration
writeTimeout time.Duration
logger *slog.Logger
}
type Option func(*Server)
func WithAddr(addr string) Option {
return func(s *Server) {
s.addr = addr
}
}
func WithReadTimeout(d time.Duration) Option {
return func(s *Server) {
s.readTimeout = d
}
}
func WithLogger(l *slog.Logger) Option {
return func(s *Server) {
s.logger = l
}
}
func NewServer(opts ...Option) *Server {
s := &Server{
addr: ":8080", // sensible defaults
readTimeout: 5 * time.Second,
writeTimeout: 10 * time.Second,
logger: slog.Default(),
}
for _, opt := range opts {
opt(s)
}
return s
}
// Usage:
srv := NewServer(
WithAddr(":9090"),
WithReadTimeout(10*time.Second),
)
// Use functional options when:
// - Many optional parameters with sensible defaults
// - API evolves over time (new options don't break callers)
// - Options need validation or side effects
// Use config struct when:
// - Most fields are required
// - Configuration is loaded from file/env (easy to deserialize)
// - No need for default values
type Config struct {
Addr string `yaml:"addr"`
DBUrl string `yaml:"db_url"`
LogLevel slog.Level `yaml:"log_level"`
}
Every exported type with invariants needs a constructor.
// ✅ Good — constructor enforces invariants
func NewUserService(repo UserRepository, logger *slog.Logger) (*UserService, error) {
if repo == nil {
return nil, errors.New("user service: nil repository")
}
if logger == nil {
return nil, errors.New("user service: nil logger")
}
return &UserService{repo: repo, logger: logger}, nil
}
// ❌ Bad — struct literal with no validation
svc := &UserService{} // nil dependencies → panic at runtime
// ✅ Good — constructor returns error
func NewEmailAddress(raw string) (EmailAddress, error) {
if !isValidEmail(raw) {
return EmailAddress{}, fmt.Errorf("invalid email: %s", raw)
}
return EmailAddress{value: raw}, nil
}
Use when you need to create different implementations of an interface based on runtime configuration.
type Store interface {
Get(ctx context.Context, key string) (string, error)
Set(ctx context.Context, key, value string) error
}
func NewStore(cfg Config) (Store, error) {
switch cfg.StoreType {
case "redis":
return newRedisStore(cfg.RedisAddr)
case "memory":
return newMemoryStore(), nil
case "postgres":
return newPostgresStore(cfg.DatabaseURL)
default:
return nil, fmt.Errorf("unknown store type: %s", cfg.StoreType)
}
}
Return the interface, not a concrete type. The factory is the only place that knows about concrete implementations.
Swap behavior at runtime by injecting function types or interfaces.
type RetryStrategy func(attempt int) time.Duration
func ExponentialBackoff(base time.Duration) RetryStrategy {
return func(attempt int) time.Duration {
return base * time.Duration(1<<uint(attempt))
}
}
func ConstantDelay(d time.Duration) RetryStrategy {
return func(_ int) time.Duration {
return d
}
}
func Retry(ctx context.Context, maxAttempts int, strategy RetryStrategy, fn func() error) error {
var err error
for i := 0; i < maxAttempts; i++ {
if err = fn(); err == nil {
return nil
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(strategy(i)):
}
}
return fmt.Errorf("after %d attempts: %w", maxAttempts, err)
}
type Notifier interface {
Notify(ctx context.Context, event Event) error
}
type SlackNotifier struct { webhookURL string }
type EmailNotifier struct { smtpClient *smtp.Client }
type NoopNotifier struct{}
// Each implements Notifier. Inject the right one at startup.
Wrap behavior around a core function or interface.
type Middleware func(http.Handler) http.Handler
func Chain(handler http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
// Usage:
handler := Chain(appHandler, Recoverer, RequestID, Logger, Auth)
type UserRepository interface {
GetByID(ctx context.Context, id string) (*User, error)
}
// Logging decorator
type loggingUserRepo struct {
next UserRepository
logger *slog.Logger
}
func NewLoggingUserRepo(next UserRepository, logger *slog.Logger) UserRepository {
return &loggingUserRepo{next: next, logger: logger}
}
func (r *loggingUserRepo) GetByID(ctx context.Context, id string) (*User, error) {
start := time.Now()
user, err := r.next.GetByID(ctx, id)
r.logger.Info("GetByID",
slog.String("id", id),
slog.Duration("duration", time.Since(start)),
slog.Any("error", err),
)
return user, err
}
Stack decorators: cache → logging → metrics → actual repo.
For operations that can return a value or an error in concurrent pipelines:
type Result[T any] struct {
Value T
Err error
}
func fetchAll(ctx context.Context, ids []string) []Result[User] {
results := make([]Result[User], len(ids))
var wg sync.WaitGroup
for i, id := range ids {
wg.Add(1)
go func(i int, id string) {
defer wg.Done()
user, err := fetchUser(ctx, id)
results[i] = Result[User]{Value: user, Err: err}
}(i, id)
}
wg.Wait()
return results
}
func processFile(path string) error {
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("open %s: %w", path, err)
}
defer f.Close()
// process file...
return nil
}
func migrate(ctx context.Context, srcDSN, dstDSN string) error {
src, err := sql.Open("postgres", srcDSN)
if err != nil {
return fmt.Errorf("open source: %w", err)
}
defer src.Close()
dst, err := sql.Open("postgres", dstDSN)
if err != nil {
return fmt.Errorf("open dest: %w", err)
}
defer dst.Close()
// defers execute LIFO: dst.Close() first, then src.Close()
return doMigration(ctx, src, dst)
}
// ✅ Good — sync.Mutex zero value is an unlocked mutex
var mu sync.Mutex
// ✅ Good — bytes.Buffer zero value is an empty buffer
var buf bytes.Buffer
// ✅ Good — slice zero value is a valid empty slice
var users []User // nil slice works with append, len, range
// When zero value is a valid input, use pointer or custom type
type Temperature struct {
Celsius float64
IsSet bool
}
// Or use a pointer
func SetThreshold(t *float64) { // nil means "not configured"
if t != nil {
applyThreshold(*t)
}
}
// ❌ God interface — too many methods
type Service interface {
GetUser(ctx context.Context, id string) (*User, error)
CreateUser(ctx context.Context, u *User) error
DeleteUser(ctx context.Context, id string) error
ListOrders(ctx context.Context, userID string) ([]Order, error)
// 20 more methods...
}
// → Split into focused interfaces: UserReader, UserWriter, OrderLister
// ❌ Premature abstraction — interface for one implementation
type UserCache interface {
Get(key string) (*User, bool)
Set(key string, user *User)
}
// If there's only ever one implementation, use the concrete type.
// Extract an interface when a second consumer or implementation appears.
// ❌ Java-style inheritance simulation
type BaseService struct { ... }
type UserService struct { BaseService } // embedding is NOT inheritance
// → Use composition: UserService has a dependency, not a parent.
func(http.Handler) http.Handler signaturedefer used for all resource cleanup (files, connections, locks)