Interface Best Practices

Overview

Well-designed interfaces make Go code flexible, testable, and maintainable.

Keep Interfaces Small

// Good: small, focused
type Reader interface {
    Read([]byte) (int, error)
}

// Avoid: too many methods
type DataProcessor interface {
    Read([]byte) (int, error)
    Write([]byte) (int, error)
    Close() error
    Flush() error
    Seek(int64, int) (int64, error)
    // ...10 more methods
}

Define Interfaces at Consumer

// In the package that USES the interface
package myapp

type Storage interface {
    Save(data []byte) error
}

func Process(s Storage) {
    s.Save([]byte("data"))
}

// NOT in the package that implements it

Accept Interfaces, Return Structs

// Accept interface (flexible)
func ParseJSON(r io.Reader) (*Config, error)

// Return concrete type (clear)
func NewFileReader(path string) *FileReader

Interface Composition

type Reader interface {
    Read([]byte) (int, error)
}

type Closer interface {
    Close() error
}

type ReadCloser interface {
    Reader
    Closer
}

Avoid Interface Pollution

// Don't create interfaces for single implementations
// Just use the concrete type

// Unnecessary
type UserService interface {
    GetUser(id int) *User
}
type userServiceImpl struct{}

// Better: just use the struct directly
type UserService struct{}
func (s *UserService) GetUser(id int) *User

Testing with Interfaces

type EmailSender interface {
    Send(to, subject, body string) error
}

// Production
type SMTPSender struct{}
func (s *SMTPSender) Send(to, subject, body string) error

// Test mock
type MockSender struct {
    SentEmails []string
}
func (m *MockSender) Send(to, subject, body string) error {
    m.SentEmails = append(m.SentEmails, to)
    return nil
}

Summary

Practice Reason
Small interfaces Easy to implement and mock
Consumer-defined Loose coupling
Accept interfaces Flexibility
Return structs Clarity
Compose Build from small pieces