Go Interfaces: Small is Beautiful
After a year of writing Go, I’ve learned that the best interfaces are small. Really small.
The Go Way
In Go, interfaces are implicit. You don’t declare that a type implements an interface:
type Reader interface {
Read(p []byte) (n int, err error)
}
type File struct {
// ...
}
func (f *File) Read(p []byte) (n int, err error) {
// File automatically implements Reader
}
Keep Interfaces Small
The best interfaces have 1-2 methods:
// Good - single method
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// Compose them
type ReadWriter interface {
Reader
Writer
}
Accept Interfaces, Return Structs
// Good
func ProcessData(r io.Reader) (*Result, error) {
// ...
}
// Bad
func ProcessData(f *os.File) (*Result, error) {
// Too specific
}
This makes your code more testable and flexible.
Real Example
We refactored our storage layer:
Before:
type Storage struct {
db *sql.DB
}
func (s *Storage) Save(data []byte) error {
// ...
}
func ProcessAndSave(s *Storage, data []byte) error {
processed := process(data)
return s.Save(processed)
}
After:
type Saver interface {
Save(data []byte) error
}
func ProcessAndSave(s Saver, data []byte) error {
processed := process(data)
return s.Save(processed)
}
Now we can easily test with a mock:
type MockSaver struct {
saved []byte
}
func (m *MockSaver) Save(data []byte) error {
m.saved = data
return nil
}
func TestProcessAndSave(t *testing.T) {
mock := &MockSaver{}
ProcessAndSave(mock, []byte("test"))
// Assert on mock.saved
}
Interface Segregation
Don’t create fat interfaces:
// Bad - too many methods
type UserService interface {
Create(user *User) error
Update(user *User) error
Delete(id string) error
FindByID(id string) (*User, error)
FindByEmail(email string) (*User, error)
List() ([]*User, error)
Count() (int, error)
}
// Good - split by responsibility
type UserCreator interface {
Create(user *User) error
}
type UserFinder interface {
FindByID(id string) (*User, error)
FindByEmail(email string) (*User, error)
}
type UserLister interface {
List() ([]*User, error)
}
The Empty Interface
interface{} means “any type”. Use sparingly:
// Bad - loses type safety
func Process(data interface{}) {
// Have to type assert
}
// Good - use generics (Go 1.18+) or specific types
func ProcessString(data string) {
// Type safe
}
Common Patterns
1. io.Reader/Writer
func SaveToFile(r io.Reader, filename string) error {
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, r)
return err
}
// Works with files, network, strings, anything!
SaveToFile(strings.NewReader("data"), "file.txt")
SaveToFile(httpResponse.Body, "response.txt")
2. http.Handler
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
// Any type with ServeHTTP is a handler
type MyHandler struct{}
func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello"))
}
3. Stringer
type Stringer interface {
String() string
}
type User struct {
Name string
}
func (u *User) String() string {
return u.Name
}
// Now fmt.Println(user) uses String()
Testing with Interfaces
Interfaces make testing easy:
type EmailSender interface {
Send(to, subject, body string) error
}
type UserService struct {
emailer EmailSender
}
// Production
type SMTPEmailer struct{}
func (s *SMTPEmailer) Send(to, subject, body string) error {
// Send via SMTP
}
// Testing
type MockEmailer struct {
sent []string
}
func (m *MockEmailer) Send(to, subject, body string) error {
m.sent = append(m.sent, to)
return nil
}
When NOT to Use Interfaces
Don’t create interfaces “just in case”:
// Bad - premature abstraction
type UserRepository interface {
Save(user *User) error
}
type PostgresUserRepository struct{}
func (r *PostgresUserRepository) Save(user *User) error {
// ...
}
// If you only have one implementation, you don't need the interface yet
Wait until you have a second implementation or need to mock for testing.
The Verdict
- Keep interfaces small (1-2 methods)
- Accept interfaces, return structs
- Don’t create interfaces prematurely
- Use standard library interfaces when possible
- Interfaces make testing easy
Go’s implicit interfaces are one of my favorite features. They encourage small, focused abstractions.
Questions? Let me know!