Interfaces and Polymorphism
40 minInterfaces 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())
}