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 itAccept Interfaces, Return Structs
// Accept interface (flexible)
func ParseJSON(r io.Reader) (*Config, error)
// Return concrete type (clear)
func NewFileReader(path string) *FileReaderInterface 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) *UserTesting 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 |