1. go
  2. /data structures
  3. /interfaces

Understanding Interfaces in Go

Interfaces define behavior by specifying a set of methods. This guide covers everything you need to know about working with interfaces effectively in Go.

Interface Basics

Interface Declaration

// Basic interface
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Interface with multiple methods
type Writer interface {
    Write(p []byte) (n int, err error)
}

// Combining interfaces
type ReadWriter interface {
    Reader
    Writer
}

Interface Implementation

// Implementing an interface
type FileReader struct {
    filepath string
}

// Implicitly implements Reader interface
func (f *FileReader) Read(p []byte) (n int, err error) {
    // Implementation
    return len(p), nil
}

// Using the interface
var reader Reader
reader = &FileReader{filepath: "data.txt"}
data := make([]byte, 100)
n, err := reader.Read(data)

Empty Interface

// Empty interface
type any interface{}

// Function accepting any type
func PrintValue(v interface{}) {
    fmt.Printf("Value: %v\n", v)
}

// Usage
PrintValue(42)
PrintValue("hello")
PrintValue(struct{ name string }{"John"})

Type Assertions

Basic Type Assertion

// Type assertion
var i interface{} = "hello"

str, ok := i.(string)
if ok {
    fmt.Printf("String value: %s\n", str)
} else {
    fmt.Println("Not a string")
}

// Panic if type assertion fails without ok check
str = i.(string)  // Will panic if i is not a string

Type Switches

func processValue(v interface{}) {
    switch x := v.(type) {
    case string:
        fmt.Printf("String: %s\n", x)
    case int:
        fmt.Printf("Integer: %d\n", x)
    case bool:
        fmt.Printf("Boolean: %v\n", x)
    default:
        fmt.Printf("Unknown type: %T\n", x)
    }
}

// Usage
processValue("hello")
processValue(42)
processValue(true)

Interface Composition

Combining Interfaces

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
}

// Combining multiple interfaces
type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

// Implementation
type File struct {
    // ...
}

func (f *File) Read(p []byte) (n int, err error) {
    // Implementation
    return len(p), nil
}

func (f *File) Write(p []byte) (n int, err error) {
    // Implementation
    return len(p), nil
}

func (f *File) Close() error {
    // Implementation
    return nil
}

Interface Segregation

// Bad: Large interface
type Animal interface {
    Eat()
    Sleep()
    Walk()
    Fly()
    Swim()
}

// Good: Segregated interfaces
type Walker interface {
    Walk()
}

type Flyer interface {
    Fly()
}

type Swimmer interface {
    Swim()
}

// Types implement only needed interfaces
type Bird struct{}

func (b Bird) Walk() { /* ... */ }
func (b Bird) Fly()  { /* ... */ }

type Fish struct{}

func (f Fish) Swim() { /* ... */ }

Common Interfaces

Stringer Interface

// fmt.Stringer interface
type Stringer interface {
    String() string
}

// Implementation
type Person struct {
    Name string
    Age  int
}

func (p Person) String() string {
    return fmt.Sprintf("%s (%d years)", p.Name, p.Age)
}

// Usage
person := Person{Name: "John", Age: 30}
fmt.Println(person)  // Uses String() method

Error Interface

// error interface
type error interface {
    Error() string
}

// Custom error type
type ValidationError struct {
    Field string
    Issue string
}

func (v ValidationError) Error() string {
    return fmt.Sprintf("%s: %s", v.Field, v.Issue)
}

// Usage
func validate(age int) error {
    if age < 0 {
        return ValidationError{
            Field: "age",
            Issue: "must be positive",
        }
    }
    return nil
}

Sort Interface

// sort.Interface
type Interface interface {
    Len() int
    Less(i, j int) bool
    Swap(i, j int)
}

// Implementation for custom type
type Person struct {
    Name string
    Age  int
}

type ByAge []Person

func (a ByAge) Len() int           { return len(a) }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
func (a ByAge) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }

// Usage
people := []Person{
    {"Bob", 31},
    {"John", 42},
    {"Michael", 17},
}
sort.Sort(ByAge(people))

Best Practices

1. Interface Design

// Good: Small, focused interfaces
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

// Bad: Kitchen sink interface
type DoEverything interface {
    Read(p []byte) (n int, err error)
    Write(p []byte) (n int, err error)
    Close() error
    Flush() error
    String() string
    // ... many more methods
}

2. Accept Interfaces, Return Structs

// Good: Accept interface
func ProcessReader(r Reader) error {
    // Process any type that implements Reader
    return nil
}

// Good: Return concrete type
func NewBufferedReader(r Reader) *BufferedReader {
    return &BufferedReader{reader: r}
}

// Usage
file := &File{}
ProcessReader(file)
buf := NewBufferedReader(file)

3. Interface Satisfaction

// Compile-time interface check
var _ Reader = (*MyReader)(nil)
var _ Writer = (*MyWriter)(nil)

// Implementation
type MyReader struct{}

func (r *MyReader) Read(p []byte) (n int, err error) {
    // Implementation
    return len(p), nil
}

type MyWriter struct{}

func (w *MyWriter) Write(p []byte) (n int, err error) {
    // Implementation
    return len(p), nil
}

Common Patterns

1. Decorator Pattern

type Logger interface {
    Log(message string)
}

// Base implementation
type ConsoleLogger struct{}

func (l ConsoleLogger) Log(message string) {
    fmt.Println(message)
}

// Decorator
type TimestampLogger struct {
    logger Logger
}

func (t TimestampLogger) Log(message string) {
    t.logger.Log(time.Now().Format(time.RFC3339) + ": " + message)
}

// Usage
logger := TimestampLogger{
    logger: ConsoleLogger{},
}
logger.Log("Hello")  // 2024-03-19T10:30:00Z: Hello

2. Strategy Pattern

type PaymentProcessor interface {
    Process(amount float64) error
}

type CreditCardProcessor struct{}

func (c CreditCardProcessor) Process(amount float64) error {
    fmt.Printf("Processing %.2f via credit card\n", amount)
    return nil
}

type PayPalProcessor struct{}

func (p PayPalProcessor) Process(amount float64) error {
    fmt.Printf("Processing %.2f via PayPal\n", amount)
    return nil
}

// Usage
func ProcessPayment(processor PaymentProcessor, amount float64) error {
    return processor.Process(amount)
}

3. Factory Pattern

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

func NewStorage(kind string) Storage {
    switch kind {
    case "file":
        return &FileStorage{}
    case "memory":
        return &MemoryStorage{}
    default:
        return &NullStorage{}
    }
}

// Usage
storage := NewStorage("file")
storage.Save([]byte("data"))

Testing with Interfaces

1. Mock Implementation

type DataStore interface {
    Get(key string) (string, error)
    Set(key, value string) error
}

// Mock implementation for testing
type MockDataStore struct {
    data map[string]string
}

func (m *MockDataStore) Get(key string) (string, error) {
    if value, ok := m.data[key]; ok {
        return value, nil
    }
    return "", fmt.Errorf("key not found: %s", key)
}

func (m *MockDataStore) Set(key, value string) error {
    m.data[key] = value
    return nil
}

// Test
func TestDataStore(t *testing.T) {
    store := &MockDataStore{
        data: make(map[string]string),
    }
    
    store.Set("key", "value")
    value, err := store.Get("key")
    
    if err != nil {
        t.Errorf("unexpected error: %v", err)
    }
    if value != "value" {
        t.Errorf("got %s, want value", value)
    }
}

2. Test Doubles

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

// Test double
type TestEmailSender struct {
    sent []struct {
        to      string
        subject string
        body    string
    }
}

func (t *TestEmailSender) Send(to, subject, body string) error {
    t.sent = append(t.sent, struct {
        to      string
        subject string
        body    string
    }{to, subject, body})
    return nil
}

// Test
func TestEmailNotification(t *testing.T) {
    sender := &TestEmailSender{}
    service := NewNotificationService(sender)
    
    service.NotifyUser("[email protected]", "Test")
    
    if len(sender.sent) != 1 {
        t.Error("expected one email to be sent")
    }
}

Next Steps

  1. Learn about error handling
  2. Explore generics
  3. Study reflection
  4. Practice with design patterns

Additional Resources