Web Analytics

Interface Basics

Intermediate ~40 min read

Interfaces are Go's way of defining behavior. They enable polymorphism without inheritance, making code flexible and testable. In this lesson, you'll learn how interfaces work, how to define them, and how to use them effectively.

What is an Interface?

An interface is a collection of method signatures. Any type that implements all the methods automatically satisfies the interface:

Output
Click Run to execute your code
Interface Characteristics:
  • Implicit implementation - No "implements" keyword needed
  • Duck typing - "If it walks like a duck and quacks like a duck..."
  • Polymorphism - Different types can satisfy the same interface
  • Decoupling - Depend on behavior, not concrete types

Interface Syntax

type InterfaceName interface {
    Method1(param type) returnType
    Method2(param type) returnType
    // ... more methods
}
Best Practice: Interface names often end with "-er" (Reader, Writer, Stringer). Keep interfaces smallβ€”preferably one or two methods!

Implicit Implementation

Go doesn't require explicit declaration of interface implementation:

Output
Click Run to execute your code
Why Implicit Implementation?
  • Types can satisfy interfaces from other packages
  • No need to modify existing code
  • More flexible and decoupled
  • Easier to test (mock interfaces)
Interfaces and Polymorphism in Go

Figure: Interfaces and Polymorphism - How different types (Circle, Rectangle) can satisfy the same interface (Shape)

Interface Values

An interface value holds a value of a specific type:

var s Shape
s = Circle{Radius: 5}    // s holds a Circle
s = Rectangle{W: 3, H: 4} // s now holds a Rectangle

// Interface value has two components:
// 1. Concrete type (Circle or Rectangle)
// 2. Concrete value (the actual data)

Nil Interface Values

var s Shape  // nil interface (both type and value are nil)

// Calling method on nil interface panics
s.Area()  // panic!

// Check for nil
if s != nil {
    s.Area()
}

The Empty Interface

The empty interface interface{} can hold values of any type:

Output
Click Run to execute your code
Empty Interface Uses:
  • Functions that accept any type (like fmt.Println)
  • Generic data structures (before Go 1.18 generics)
  • JSON unmarshaling to unknown structures
Note: Go 1.18+ introduced generics with any as an alias for interface{}. Use any in new code for clarity!

Interface Composition

Interfaces can be composed from other 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
}

// Composed interface
type ReadWriter interface {
    Reader
    Writer
}

type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}
Pro Tip: Compose small interfaces into larger ones. This follows the "Interface Segregation Principle"β€”clients shouldn't depend on methods they don't use.

Practical Interface Patterns

1. Dependency Injection

// Define interface for what you need
type DataStore interface {
    Save(data string) error
    Load() (string, error)
}

// Business logic depends on interface
type Service struct {
    store DataStore
}

func (s *Service) Process(data string) error {
    return s.store.Save(data)
}

// Different implementations
type FileStore struct { /* ... */ }
func (f *FileStore) Save(data string) error { /* ... */ }
func (f *FileStore) Load() (string, error) { /* ... */ }

type MemoryStore struct { /* ... */ }
func (m *MemoryStore) Save(data string) error { /* ... */ }
func (m *MemoryStore) Load() (string, error) { /* ... */ }

2. Testing with Mocks

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

type UserService struct {
    emailer EmailSender
}

// Test code
type MockEmailer struct {
    sentEmails []string
}

func (m *MockEmailer) Send(to, subject, body string) error {
    m.sentEmails = append(m.sentEmails, to)
    return nil
}

// Test
func TestUserService(t *testing.T) {
    mock := &MockEmailer{}
    service := UserService{emailer: mock}
    // Test without sending real emails!
}

Common Mistakes

1. Interface pollution (too many interfaces)

// ❌ Wrong - unnecessary interface
type UserGetter interface {
    GetUser(id int) User
}

type UserService struct{}
func (s *UserService) GetUser(id int) User { /* ... */ }

// Only one implementation? Don't need interface!

// βœ… Correct - use concrete type
type UserService struct{}
func (s *UserService) GetUser(id int) User { /* ... */ }

// Create interface only when you need abstraction

2. Pointer vs value receivers

type Printer interface {
    Print()
}

type Document struct {
    content string
}

// Pointer receiver
func (d *Document) Print() {
    fmt.Println(d.content)
}

// ❌ Wrong - value doesn't satisfy interface
var p Printer = Document{content: "test"}  // Error!

// βœ… Correct - use pointer
var p Printer = &Document{content: "test"}  // OK

3. Returning concrete type instead of interface

// ❌ Less flexible
func NewUserService() *UserService {
    return &UserService{}
}

// βœ… Better - return interface
type UserStore interface {
    GetUser(id int) User
}

func NewUserService() UserStore {
    return &UserService{}
}
// Callers depend on interface, not concrete type

Exercise: Payment Processor

Task: Create a payment processing system with interfaces.

Requirements:

  • Define a PaymentMethod interface with Pay(amount float64) error
  • Implement CreditCard and PayPal types
  • Create a processPayment function that accepts the interface
  • Test with both payment methods
Show Solution
package main

import (
    "fmt"
)

// PaymentMethod interface
type PaymentMethod interface {
    Pay(amount float64) error
}

// CreditCard implementation
type CreditCard struct {
    Number string
    Name   string
}

func (c CreditCard) Pay(amount float64) error {
    fmt.Printf("Charging $%.2f to credit card %s\n", amount, c.Number)
    return nil
}

// PayPal implementation
type PayPal struct {
    Email string
}

func (p PayPal) Pay(amount float64) error {
    fmt.Printf("Charging $%.2f to PayPal account %s\n", amount, p.Email)
    return nil
}

// Bitcoin implementation (bonus)
type Bitcoin struct {
    WalletAddress string
}

func (b Bitcoin) Pay(amount float64) error {
    fmt.Printf("Sending $%.2f worth of Bitcoin to %s\n", amount, b.WalletAddress)
    return nil
}

// Process payment using any payment method
func processPayment(method PaymentMethod, amount float64) error {
    fmt.Printf("Processing payment of $%.2f...\n", amount)
    return method.Pay(amount)
}

func main() {
    // Test with different payment methods
    cc := CreditCard{
        Number: "**** **** **** 1234",
        Name:   "Alice Smith",
    }
    
    pp := PayPal{
        Email: "[email protected]",
    }
    
    btc := Bitcoin{
        WalletAddress: "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
    }
    
    // Process payments
    processPayment(cc, 99.99)
    fmt.Println()
    processPayment(pp, 49.99)
    fmt.Println()
    processPayment(btc, 199.99)
}

Summary

  • Interfaces define behavior through method signatures
  • Implicit implementation - no "implements" keyword needed
  • Any type that has the methods satisfies the interface
  • Empty interface (interface{} or any) accepts any type
  • Interface composition builds larger interfaces from smaller ones
  • Keep interfaces small - preferably 1-2 methods
  • Accept interfaces, return structs (usually)
  • Use interfaces for abstraction, testing, and flexibility

What's Next?

Now that you understand interface basics, you're ready to learn about Type Switches & Common Interfaces. You'll discover how to work with interface values at runtime and explore Go's standard library interfaces.