interfaces

Contents

Roadmap info from roadmap website

Interfaces

An interface in Go, is a type that defines a set of methods. If we have a type (e.g. struct) that implements that set of methods, then we have a type that implements this interface.

Visit the following resources to learn more:

Tips

  • Design interfaces based on behavior, not data.
  • Keep interfaces small and focused.
  • Rely on implicit interface satisfaction to reduce boilerplate.
  • Use interfaces as function parameters to increase flexibility.
  • Return concrete types when possible to reduce ambiguity.
  • Ensure the zero value of types implementing interfaces is useful.
  • Avoid single-implementer interfaces unless necessary.
  • Compose interfaces to build larger abstractions from smaller ones.
  • Document interfaces to clarify their contract.
  • Use interfaces in tests for better mockability and test coverage.
  • Keep interfaces unexported unless they need to be public.
  • Avoid creating large, monolithic interfaces that are hard to implement.

Best Practices for Using Interfaces in Go

Interfaces in Go provide a way to define behavior through method signatures without specifying how those behaviors are implemented. Proper use of interfaces can lead to more modular, testable, and maintainable code. Here are some best practices for using interfaces in Go:

1. Define Interfaces Based on Behavior, Not Data

  • Focus on Method Sets: Design interfaces to represent a specific behavior or set of related behaviors, rather than tying them to particular data structures. Interfaces should capture β€œwhat something does” rather than β€œwhat something is.”

Example:

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

The Reader interface is defined based on the behavior of reading, not tied to any specific data structure.

2. Keep Interfaces Small

  • Single Responsibility: Favor small, focused interfaces with one or two methods. This adheres to the Single Responsibility Principle and makes your interfaces easier to implement, mock, and test.

Example:

type Closer interface {
    Close() error
}

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

These interfaces are small and focused, making them easy to implement and compose.

3. Use Interface Satisfaction Implicitly

  • Implicit Satisfaction: Go’s interfaces are satisfied implicitly, meaning a type doesn’t need to declare that it implements an interface. Instead, if a type implements the methods defined by an interface, it automatically satisfies that interface.

Example:

type File struct {}

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

// File implicitly implements the Reader interface.

4. Prefer Using Interfaces for Parameters, Not Returns

  • Dependency Injection: When defining functions or methods, prefer accepting interfaces as parameters rather than returning them. This allows for greater flexibility and easier testing.

Example:

func process(r io.Reader) error {
    // Function works with any type that implements io.Reader
    return nil
}

5. Return Concrete Types When Possible

  • Concrete Return Types: When returning values from functions, it’s often better to return concrete types rather than interfaces. This provides more clarity and reduces ambiguity about the type of the returned value.

Example:

func NewFile(name string) *File {
    return &File{name: name}
}

6. Design for the Zero Value

  • Useful Zero Value: Ensure that the zero value of your types that implement interfaces is useful. This allows for more flexible and error-resistant code.

Example:

type Counter struct {
    count int
}

func (c *Counter) Increment() {
    c.count++
}

// Counter's zero value is ready to use without initialization.

7. Avoid Creating Interface Types for Only One Implementer

  • One Implementer Pitfall: If you have an interface with only one concrete implementation, reconsider whether the interface is necessary. Interfaces are most useful when they abstract over multiple types.

Example:

type Database interface {
    Query(query string) ([]Result, error)
}

// If there's only one type implementing Database, the interface might be unnecessary.

8. Use Interface Composition

  • Compose Small Interfaces: Build larger interfaces by composing smaller, focused interfaces. This provides flexibility and reusability.

Example:

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

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

type ReadWriter interface {
    Reader
    Writer
}

9. Document Interfaces Clearly

  • Document Expected Behavior: Clearly document the expected behavior of each method in an interface. This helps implementers understand the contract they are agreeing to by implementing the interface.

Example:

type Storage interface {
    // Save persists the data and returns an error if the operation fails.
    Save(data []byte) error

    // Load retrieves data based on the key and returns an error if not found.
    Load(key string) ([]byte, error)
}

10. Use Interfaces in Tests for Mocks and Stubs

  • Testing Flexibility: Use interfaces to decouple your code from concrete implementations, making it easier to use mocks and stubs in tests. This improves the testability of your code.

Example:

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

// Production implementation
type SMTPSender struct {}

func (s *SMTPSender) Send(to string, body string) error {
    // Actual sending logic
    return nil
}

// Test implementation
type MockSender struct {
    SentMessages []string
}

func (m *MockSender) Send(to string, body string) error {
    m.SentMessages = append(m.SentMessages, body)
    return nil
}

11. Avoid Exporting Unnecessary Interfaces

  • Package-Private Interfaces: Keep interfaces unexported unless they are meant to be implemented by types outside the package. This reduces the API surface and prevents misuse.

Example:

// unexported interface
type sorter interface {
    Sort([]int)
}

12. Avoid Interface Pollution

  • No β€œGod” Interfaces: Avoid creating large interfaces with many unrelated methods. This leads to interface pollution, making it harder for implementers and clients to use.

Example:

// Bad practice: too many methods in one interface
type Everything interface {
    Read(p []byte) (n int, err error)
    Write(p []byte) (n int, err error)
    Close() error
    Seek(offset int64, whence int) (int64, error)
}
#ready #online #reviewed #go #summary #informatic #data-structure #data-representation #advanced #interface #methods #22