Back to Curriculum

Interfaces and Polymorphism

📚 Lesson 7 of 16 ⏱️ 40 min

Interfaces and Polymorphism

40 min

Interfaces in Go define behavior through method signatures, specifying what methods a type must implement. Interfaces enable polymorphism—different types can be used interchangeably if they implement the same interface. Go uses structural typing (duck typing)—types implement interfaces implicitly by having the required methods. Interfaces are central to Go's design philosophy. Understanding interfaces enables flexible, decoupled code.

Go uses structural typing, meaning types implement interfaces implicitly if they have the required methods. You don't explicitly declare that a type implements an interface. This makes interfaces flexible and easy to use. The `io.Reader` and `io.Writer` interfaces are examples of this pattern. Understanding structural typing helps you write flexible, testable code.

The empty interface `interface{}` (or `any` in Go 1.18+) can hold any type, making it useful for generic operations. However, empty interfaces should be used sparingly—they lose type safety. Type assertions and type switches are used to extract concrete types from empty interfaces. Understanding empty interfaces helps you work with unknown types.

Type assertions allow you to extract a concrete type from an interface: `value, ok := i.(Type)`. Type switches use `switch v := i.(type)` to handle multiple types. Type assertions and switches are essential for working with interfaces and empty interfaces. Understanding these features helps you safely work with interfaces.

Interface composition allows creating new interfaces by embedding existing ones. For example, `io.ReadWriter` embeds `io.Reader` and `io.Writer`. Interface composition enables building complex interfaces from simple ones. Understanding interface composition helps you design flexible APIs.

Best practices include keeping interfaces small (prefer many small interfaces over few large ones), accepting interfaces and returning concrete types, using interface composition, avoiding empty interfaces when possible, and designing interfaces based on behavior, not data. Understanding interfaces enables you to write flexible, maintainable Go code.

Key Concepts

  • Interfaces define behavior through method signatures.
  • Go uses structural typing—types implement interfaces implicitly.
  • The empty interface {} (or any) can hold any type.
  • Type assertions and type switches extract concrete types.
  • Interface composition builds complex interfaces from simple ones.

Learning Objectives

Master

  • Defining and using interfaces for polymorphism
  • Understanding structural typing in Go
  • Using type assertions and type switches
  • Composing interfaces for flexible design

Develop

  • Interface design thinking
  • Understanding polymorphism in Go
  • Designing flexible, testable APIs

Tips

  • Keep interfaces small—prefer many small interfaces over few large ones.
  • Accept interfaces, return concrete types.
  • Use interface composition to build complex interfaces.
  • Avoid empty interfaces when possible—they lose type safety.

Common Pitfalls

  • Creating interfaces that are too large or too specific.
  • Not understanding structural typing, trying to explicitly implement interfaces.
  • Overusing empty interfaces, losing type safety.
  • Not checking type assertion success, causing panics.

Summary

  • Interfaces define behavior and enable polymorphism in Go.
  • Go uses structural typing—types implement interfaces implicitly.
  • Type assertions and switches extract concrete types from interfaces.
  • Interface composition builds complex interfaces from simple ones.
  • Understanding interfaces enables flexible, maintainable code.

Exercise

Demonstrate interfaces, type assertions, and polymorphism.

package main

import (
    "fmt"
    "strconv"
)

// Interface for printable objects
type Printable interface {
    Print() string
}

// Interface for objects that can be converted to string
type Stringer interface {
    String() string
}

// Person implements Printable
type Person struct {
    Name string
    Age  int
}

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

func (p Person) String() string {
    return p.Print()
}

// Product implements Printable
type Product struct {
    Name  string
    Price float64
}

func (p Product) Print() string {
    return fmt.Sprintf("Product: %s, Price: $%.2f", p.Name, p.Price)
}

func (p Product) String() string {
    return p.Print()
}

// Function that works with any Printable
func printObject(p Printable) {
    fmt.Println(p.Print())
}

// Type assertion example
func processInterface(i interface{}) {
    switch v := i.(type) {
    case string:
        fmt.Printf("String: %s
", v)
    case int:
        fmt.Printf("Integer: %d
", v)
    case Person:
        fmt.Printf("Person: %s
", v.Name)
    default:
        fmt.Printf("Unknown type: %T
", v)
    }
}

// Function that accepts any type (empty interface)
func describe(i interface{}) {
    fmt.Printf("Type: %T, Value: %v
", i, i)
}

func main() {
    // Creating objects that implement Printable
    person := Person{Name: "Alice", Age: 25}
    product := Product{Name: "Laptop", Price: 999.99}
    
    // Polymorphism - same function works with different types
    printObject(person)
    printObject(product)
    
    // Type assertions
    var i interface{} = "Hello"
    str, ok := i.(string)
    if ok {
        fmt.Printf("String value: %s
", str)
    }
    
    // Type switches
    processInterface("Hello")
    processInterface(42)
    processInterface(person)
    
    // Empty interface
    describe("Hello")
    describe(42)
    describe(true)
    describe(person)
    
    // Interface composition
    type Reader interface {
        Read() string
    }
    
    type Writer interface {
        Write(string)
    }
    
    type ReadWriter interface {
        Reader
        Writer
    }
    
    // String conversion
    var stringer Stringer = person
    fmt.Printf("String representation: %s
", stringer.String())
}

Code Editor

Output