Interface Design and Composition
Small, Focused Interfaces
// Single-method interfaces (idiomatic Go)
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// Interface composition
type ReadCloser interface {
Reader
Closer
}
type WriteCloser interface {
Writer
Closer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
Accept Interfaces, Return Structs
package storage
import "io"
// Storage is the concrete type (struct)
type Storage struct {
baseDir string
}
// NewStorage returns a concrete type
func NewStorage(baseDir string) *Storage {
return &Storage{baseDir: baseDir}
}
// SaveFile accepts an interface for flexibility
func (s *Storage) SaveFile(filename string, data io.Reader) error {
// Implementation can work with any Reader
// (file, network, buffer, etc.)
return nil
}
// Usage allows dependency injection
type Uploader interface {
SaveFile(filename string, data io.Reader) error
}
type Service struct {
uploader Uploader // Accept interface
}
// NewService accepts interface for testing flexibility
func NewService(uploader Uploader) *Service {
return &Service{uploader: uploader}
}
io.Reader and io.Writer Patterns
import (
"io"
"strings"
)
// Chain readers with io.MultiReader
func combineReaders() io.Reader {
r1 := strings.NewReader("Hello ")
r2 := strings.NewReader("World")
return io.MultiReader(r1, r2)
}
// Tee reader for duplicating reads
func duplicateRead(r io.Reader, w io.Writer) io.Reader {
return io.TeeReader(r, w) // Writes to w while reading from r
}
// Limit reader to prevent reading too much
func limitedRead(r io.Reader, n int64) io.Reader {
return io.LimitReader(r, n)
}
// Custom Reader implementation
type UppercaseReader struct {
src io.Reader
}
func (u *UppercaseReader) Read(p []byte) (n int, err error) {
n, err = u.src.Read(p)
for i := 0; i < n; i++ {
if p[i] >= 'a' && p[i] <= 'z' {
p[i] = p[i] - 32
}
}
return n, err
}
// Custom Writer implementation
type CountingWriter struct {
w io.Writer
count int64
}
func (cw *CountingWriter) Write(p []byte) (n int, err error) {
n, err = cw.w.Write(p)
cw.count += int64(n)
return n, err
}
func (cw *CountingWriter) BytesWritten() int64 {
return cw.count
}
Embedding for Composition
import "sync"
// Embed to extend behavior
type SafeCounter struct {
mu sync.Mutex
m map[string]int
}
func (sc *SafeCounter) Inc(key string) {
sc.mu.Lock()
defer sc.mu.Unlock()
sc.m[key]++
}
// Embed interface to add default behavior
type Logger interface {
Log(msg string)
}
type NoOpLogger struct{}
func (NoOpLogger) Log(msg string) {}
type Service struct {
Logger // Embedded interface (default implementation can be provided)
}
func NewService(logger Logger) *Service {
if logger == nil {
logger = NoOpLogger{} // Provide default
}
return &Service{Logger: logger}
}
// Now Service.Log() is available
Interface Satisfaction Verification
import "io"
// Compile-time interface verification
var _ io.Reader = (*MyReader)(nil)
var _ io.Writer = (*MyWriter)(nil)
var _ io.Closer = (*MyCloser)(nil)
type MyReader struct{}
func (m *MyReader) Read(p []byte) (n int, err error) {
return 0, nil
}
type MyWriter struct{}
func (m *MyWriter) Write(p []byte) (n int, err error) {
return len(p), nil
}
type MyCloser struct{}
func (m *MyCloser) Close() error {
return nil
}
Functional Options Pattern
package server
import "time"
type Server struct {
host string
port int
timeout time.Duration
maxConns int
enableLogger bool
}
// Option is a functional option for configuring Server
type Option func(*Server)
func WithHost(host string) Option {
return func(s *Server) {
s.host = host
}
}
func WithPort(port int) Option {
return func(s *Server) {
s.port = port
}
}
func WithTimeout(timeout time.Duration) Option {
return func(s *Server) {
s.timeout = timeout
}
}
func WithMaxConnections(max int) Option {
return func(s *Server) {
s.maxConns = max
}
}
func WithLogger(enabled bool) Option {
return func(s *Server) {
s.enableLogger = enabled
}
}
// NewServer creates a server with functional options
func NewServer(opts ...Option) *Server {
// Defaults
s := &Server{
host: "localhost",
port: 8080,
timeout: 30 * time.Second,
maxConns: 100,
}
// Apply options
for _, opt := range opts {
opt(s)
}
return s
}
// Usage:
// server := NewServer(
// WithHost("0.0.0.0"),
// WithPort(9000),
// WithTimeout(60 * time.Second),
// WithLogger(true),
// )
Interface Segregation
// Bad: Fat interface
type BadRepository interface {
Create(item Item) error
Read(id string) (Item, error)
Update(item Item) error
Delete(id string) error
List() ([]Item, error)
Search(query string) ([]Item, error)
Count() (int, error)
}
// Good: Segregated interfaces
type Creator interface {
Create(item Item) error
}
type Reader interface {
Read(id string) (Item, error)
}
type Updater interface {
Update(item Item) error
}
type Deleter interface {
Delete(id string) error
}
type Lister interface {
List() ([]Item, error)
}
// Compose only what you need
type ReadWriter interface {
Reader
Creator
}
type FullRepository interface {
Creator
Reader
Updater
Deleter
Lister
}
Type Assertions and Type Switches
import "fmt"
// Safe type assertion
func processValue(v interface{}) {
// Two-value assertion (safe)
if str, ok := v.(string); ok {
fmt.Println("String:", str)
return
}
// Type switch
switch val := v.(type) {
case int:
fmt.Println("Int:", val)
case string:
fmt.Println("String:", val)
case bool:
fmt.Println("Bool:", val)
default:
fmt.Println("Unknown type")
}
}
// Check for optional interface methods
type Flusher interface {
Flush() error
}
func writeAndFlush(w io.Writer, data []byte) error {
if _, err := w.Write(data); err != nil {
return err
}
// Check if Writer also implements Flusher
if flusher, ok := w.(Flusher); ok {
return flusher.Flush()
}
return nil
}
Dependency Injection via Interfaces
package app
import "context"
// Define interfaces for dependencies
type UserRepository interface {
GetUser(ctx context.Context, id string) (*User, error)
SaveUser(ctx context.Context, user *User) error
}
type EmailSender interface {
SendEmail(ctx context.Context, to, subject, body string) error
}
// Service depends on interfaces
type UserService struct {
repo UserRepository
mailer EmailSender
}
func NewUserService(repo UserRepository, mailer EmailSender) *UserService {
return &UserService{
repo: repo,
mailer: mailer,
}
}
func (s *UserService) RegisterUser(ctx context.Context, email string) error {
user := &User{Email: email}
if err := s.repo.SaveUser(ctx, user); err != nil {
return err
}
return s.mailer.SendEmail(ctx, email, "Welcome", "Thanks for registering!")
}
// Easy to mock in tests
type MockUserRepository struct{}
func (m *MockUserRepository) GetUser(ctx context.Context, id string) (*User, error) {
return &User{ID: id}, nil
}
func (m *MockUserRepository) SaveUser(ctx context.Context, user *User) error {
return nil
}
Quick Reference
| Pattern | Use Case | Key Principle |
|---|
| Small interfaces | Flexibility | Single-method interfaces |
| Accept interfaces | Testability | Depend on abstractions |
| Return structs | Clarity | Concrete return types |
| io.Reader/Writer | I/O operations | Standard library integration |
| Embedding | Composition | Extend behavior without inheritance |
| Functional options | Configuration | Flexible constructors |
| Type assertions | Runtime checks | Safe downcasting |