Go Interfaces: The Power of Implicit Implementation
const x = () =>
<div className="...">
npm install
git commit -m
console.log()
Back to blog
golanginterfacespolymorphismadvanceddesign-patterns

Go Interfaces: The Power of Implicit Implementation

Master Go's interface system - from basic interfaces to advanced patterns like type assertions, empty interfaces, and interface composition.

10 min read
2,156 views

Understanding Go Interfaces

Go's interface system is different from most object-oriented languages. Instead of explicitly implementing interfaces, Go uses implicit implementation - if a type has all the methods an interface requires, it automatically implements that interface.

Basic Interface Syntax

go
// Define an interface
type Writer interface {
    Write([]byte) (int, error)
}

// Any type that has a Write method implements Writer
type File struct {
    name string
}

func (f File) Write(data []byte) (int, error) {
    fmt.Printf("Writing to %s: %s\n", f.name, string(data))
    return len(data), nil
}

// File automatically implements Writer!
func saveData(w Writer, data []byte) {
    w.Write(data)
}

Common Interface Patterns

1. The io.Reader Interface

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

// Custom reader
type StringReader struct {
    data string
    pos  int
}

func (sr *StringReader) Read(p []byte) (int, error) {
    if sr.pos >= len(sr.data) {
        return 0, io.EOF
    }

    n := copy(p, sr.data[sr.pos:])
    sr.pos += n
    return n, nil
}

// Usage
reader := &StringReader{data: "Hello, World!"}
buffer := make([]byte, 5)
n, err := reader.Read(buffer)
fmt.Printf("Read %d bytes: %s\n", n, string(buffer))

2. The fmt.Stringer Interface

go
type Stringer interface {
    String() string
}

type Person struct {
    Name string
    Age  int
}

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

// Now Person can be used with fmt.Print
person := Person{Name: "Alice", Age: 30}
fmt.Println(person) // Output: Alice (30 years old)

Interface Composition

Go allows you to compose interfaces by embedding them:

go
// Basic interfaces
type Reader interface {
    Read([]byte) (int, error)
}

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

type Closer interface {
    Close() error
}

// Composed interface
type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

// Or create new methods
type ReadWriter interface {
    Reader
    Writer
    Flush() error
}

Type Assertions

Type assertions allow you to extract the concrete type from an interface:

go
var i interface{} = "hello"

// Type assertion
s, ok := i.(string)
if ok {
    fmt.Printf("It's a string: %s\n", s)
} else {
    fmt.Println("Not a string")
}

// Type switch
switch v := i.(type) {
case string:
    fmt.Printf("String: %s\n", v)
case int:
    fmt.Printf("Integer: %d\n", v)
default:
    fmt.Printf("Unknown type: %T\n", v)
}

Empty Interface

The empty interface interface{} can hold any type:

go
func printAny(v interface{}) {
    switch val := v.(type) {
    case string:
        fmt.Printf("String: %s\n", val)
    case int:
        fmt.Printf("Int: %d\n", val)
    case bool:
        fmt.Printf("Bool: %t\n", val)
    default:
        fmt.Printf("Type: %T, Value: %v\n", val, val)
    }
}

// Usage
printAny("hello")
printAny(42)
printAny(true)
printAny([]int{1, 2, 3})

Advanced Interface Patterns

1. Interface Segregation

go
// Instead of one large interface
type BadWorker interface {
    Work()
    Eat()
    Sleep()
    Code()
    Design()
    Test()
}

// Use smaller, focused interfaces
type Worker interface {
    Work()
}

type Eater interface {
    Eat()
}

type Coder interface {
    Code()
}

// Compose as needed
type Developer interface {
    Worker
    Coder
}

2. Dependency Injection

go
// Define interfaces for dependencies
type UserRepository interface {
    GetUser(id int) (*User, error)
    SaveUser(user *User) error
}

type EmailService interface {
    SendEmail(to, subject, body string) error
}

// Service depends on interfaces, not concrete types
type UserService struct {
    repo  UserRepository
    email EmailService
}

func (s *UserService) CreateUser(name, email string) error {
    user := &User{Name: name, Email: email}

    if err := s.repo.SaveUser(user); err != nil {
        return err
    }

    return s.email.SendEmail(email, "Welcome!", "Welcome to our service!")
}

3. Mocking for Testing

go
// Mock implementation for testing
type MockUserRepository struct {
    users map[int]*User
}

func (m *MockUserRepository) GetUser(id int) (*User, error) {
    if user, exists := m.users[id]; exists {
        return user, nil
    }
    return nil, fmt.Errorf("user not found")
}

func (m *MockUserRepository) SaveUser(user *User) error {
    if m.users == nil {
        m.users = make(map[int]*User)
    }
    m.users[user.ID] = user
    return nil
}

// Test with mock
func TestUserService(t *testing.T) {
    mockRepo := &MockUserRepository{}
    mockEmail := &MockEmailService{}

    service := &UserService{
        repo:  mockRepo,
        email: mockEmail,
    }

    err := service.CreateUser("John", "john@example.com")
    assert.NoError(t, err)
}

Interface Values and Nil

Understanding interface values is crucial:

go
var w Writer // w is nil

// This will panic!
// w.Write([]byte("hello"))

// Check if interface is nil
if w != nil {
    w.Write([]byte("hello"))
}

// Interface with nil concrete value
var f *File = nil
var w2 Writer = f // w2 is not nil!

if w2 != nil {
    // This will panic because f is nil
    // w2.Write([]byte("hello"))
}

Generic Interfaces (Go 1.18+)

With generics, you can create more flexible interfaces:

go
type Comparable[T any] interface {
    Compare(other T) int
}

type Ordered interface {
    ~int | ~float64 | ~string
}

type Sorter[T Ordered] interface {
    Sort([]T) []T
}

// Usage
func SortSlice[T Ordered](slice []T, sorter Sorter[T]) []T {
    return sorter.Sort(slice)
}

Best Practices

1. Keep Interfaces Small

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

// Bad - too many responsibilities
type FileHandler interface {
    Read([]byte) (int, error)
    Write([]byte) (int, error)
    Close() error
    Open() error
    Delete() error
    Rename(string) error
}

2. Accept Interfaces, Return Concrete Types

go
// Good
func ProcessData(r Reader) error {
    // Process data
}

// Bad
func ProcessData(f *File) error {
    // Limited to File type only
}

3. Use Interface Composition

go
// Instead of duplicating methods
type ReadWriter interface {
    Read([]byte) (int, error)
    Write([]byte) (int, error)
}

// Compose from existing interfaces
type ReadWriter interface {
    Reader
    Writer
}

4. Document Interface Behavior

go
// Reader reads data from a source
// Read returns the number of bytes read and any error encountered
type Reader interface {
    Read([]byte) (int, error)
}

Common Interface Mistakes

  1. Accepting concrete types instead of interfaces
  2. Creating interfaces that are too large
  3. Not handling nil interface values properly
  4. Forgetting to check type assertion success

Real-World Example: HTTP Handler

go
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

type ResponseWriter interface {
    Header() Header
    Write([]byte) (int, error)
    WriteHeader(statusCode int)
}

// Custom handler
type MyHandler struct{}

func (h *MyHandler) ServeHTTP(w ResponseWriter, r *Request) {
    w.Header().Set("Content-Type", "text/plain")
    w.WriteHeader(200)
    w.Write([]byte("Hello, World!"))
}

// Usage
http.Handle("/", &MyHandler{})
http.ListenAndServe(":8080", nil)

Go's interface system is powerful and flexible. It promotes loose coupling and makes your code more testable and maintainable. Master these concepts, and you'll write more idiomatic Go code! 🚀