Enforce the Delegator pattern in Go — wrap dependencies with explicit method forwarding instead of struct embedding. Load this skill when designing Go interfaces, services, or adapters.
I enforce the Delegator pattern in Go: structs hold dependencies as named fields and explicitly forward method calls, rather than embedding types which leaks their full API surface. This produces clean, intentional interfaces where every exported method is a conscious design choice.
Embedding in Go promotes ALL methods of the embedded type to the outer type. This is dangerous because:
The Delegator pattern makes every method call explicit, giving you full control over your type's API surface.
Embedding is only acceptable for:
testing.TB)sync.Mutex as an unexported named field (mu sync.Mutex, NOT embedded)For everything else, use a named field and delegate explicitly.
// BAD: embedding leaks all of DB's methods
type UserStore struct {
*sql.DB
}
// GOOD: explicit delegation, controlled API
type UserStore struct {
db *sql.DB
}
func (s *UserStore) GetUser(ctx context.Context, id string) (*User, error) {
return scanUser(s.db.QueryRowContext(ctx, "SELECT ... WHERE id = $1", id))
}
func (s *UserStore) Close() error {
return s.db.Close()
}
The inner dependency should be an implementation detail, not part of your public API.
// BAD: exposes the delegate
type Service struct {
Logger *zap.Logger
Cache redis.Client
}
// GOOD: hides the delegate
type Service struct {
logger *zap.Logger
cache redis.Client
}
Don't blindly forward all methods. Each forwarded method is a conscious API decision.
// BAD: forwarding everything defeats the purpose
func (s *Service) Debug(msg string, fields ...zap.Field) { s.logger.Debug(msg, fields...) }
func (s *Service) Info(msg string, fields ...zap.Field) { s.logger.Info(msg, fields...) }
func (s *Service) Warn(msg string, fields ...zap.Field) { s.logger.Warn(msg, fields...) }
func (s *Service) Error(msg string, fields ...zap.Field) { s.logger.Error(msg, fields...) }
func (s *Service) DPanic(msg string, fields ...zap.Field) { s.logger.DPanic(msg, fields...) }
func (s *Service) Panic(msg string, fields ...zap.Field) { s.logger.Panic(msg, fields...) }
func (s *Service) Fatal(msg string, fields ...zap.Field) { s.logger.Fatal(msg, fields...) }
// GOOD: Service uses the logger internally, doesn't re-expose it
type Service struct {
logger *zap.Logger
}
func (s *Service) Process(ctx context.Context, req Request) error {
s.logger.Info("processing request", zap.String("id", req.ID))
// ...
}
Define small interfaces for what you need from the delegate. This makes testing easy and decouples from the concrete implementation.
// GOOD: depend on behavior, not implementation
type UserRepository interface {
GetUser(ctx context.Context, id string) (*User, error)
SaveUser(ctx context.Context, user *User) error
}
type UserService struct {
repo UserRepository
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
Use a constructor function to make dependencies explicit and validate them.
func NewOrderService(
repo OrderRepository,
payments PaymentGateway,
logger *zap.Logger,
) (*OrderService, error) {
if repo == nil {
return nil, errors.New("order repository is required")
}
if payments == nil {
return nil, errors.New("payment gateway is required")
}
if logger == nil {
logger = zap.NewNop()
}
return &OrderService{
repo: repo,
payments: payments,
logger: logger,
}, nil
}
When wrapping an interface implementation, hold the inner implementation as a field and forward calls with added behavior.
// Interface
type Cache interface {
Get(ctx context.Context, key string) ([]byte, error)
Set(ctx context.Context, key string, val []byte, ttl time.Duration) error
}
// Decorator adds metrics
type instrumentedCache struct {
inner Cache
metrics *prometheus.CounterVec
}
func NewInstrumentedCache(inner Cache, metrics *prometheus.CounterVec) Cache {
return &instrumentedCache{inner: inner, metrics: metrics}
}
func (c *instrumentedCache) Get(ctx context.Context, key string) ([]byte, error) {
c.metrics.WithLabelValues("get").Inc()
return c.inner.Get(ctx, key)
}
func (c *instrumentedCache) Set(ctx context.Context, key string, val []byte, ttl time.Duration) error {
c.metrics.WithLabelValues("set").Inc()
return c.inner.Set(ctx, key, val, ttl)
}
When adapting one interface to another, the delegator translates method signatures.
// External SDK type
type ThirdPartyMailer struct { /* ... */ }
func (m *ThirdPartyMailer) Dispatch(to, subject, html string) error { /* ... */ }
// Our interface
type Notifier interface {
Notify(ctx context.Context, recipient string, msg Message) error
}
// Adapter delegates to the SDK
type emailNotifier struct {
mailer *ThirdPartyMailer
}
func NewEmailNotifier(mailer *ThirdPartyMailer) Notifier {
return &emailNotifier{mailer: mailer}
}
func (n *emailNotifier) Notify(ctx context.Context, recipient string, msg Message) error {
html := renderTemplate(msg)
return n.mailer.Dispatch(recipient, msg.Subject, html)
}
Before completing any Go code that composes types, verify: