intermediate Step 7 of 15

Interfaces and Polymorphism

Go Programming

Interfaces and Polymorphism

Interfaces in Go are one of its most powerful and distinctive features. Unlike Java or C# where classes explicitly declare which interfaces they implement, Go interfaces are satisfied implicitly — if a type has all the methods an interface requires, it automatically implements that interface. This "duck typing" approach enables loose coupling, testability, and polymorphism without tight dependencies between packages. The standard library is designed around small, focused interfaces like io.Reader, io.Writer, and fmt.Stringer.

Defining and Implementing Interfaces

package main

import (
    "fmt"
    "math"
)

// Interface definition — just a set of method signatures
type Shape interface {
    Area() float64
    Perimeter() float64
}

// Circle implements Shape (implicitly — no "implements" keyword)
type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

// Rectangle also implements Shape
type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64      { return r.Width * r.Height }
func (r Rectangle) Perimeter() float64 { return 2 * (r.Width + r.Height) }

// Function accepting the interface — polymorphism
func printShapeInfo(s Shape) {
    fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}

func totalArea(shapes []Shape) float64 {
    total := 0.0
    for _, s := range shapes {
        total += s.Area()
    }
    return total
}

func main() {
    shapes := []Shape{
        Circle{Radius: 5},
        Rectangle{Width: 10, Height: 5},
        Circle{Radius: 3},
    }

    for _, s := range shapes {
        printShapeInfo(s)
    }

    fmt.Printf("Total area: %.2f\n", totalArea(shapes))
}

Common Standard Library Interfaces

import (
    "fmt"
    "io"
    "strings"
)

// io.Reader — anything that can be read from
// type Reader interface { Read(p []byte) (n int, err error) }

// io.Writer — anything that can be written to
// type Writer interface { Write(p []byte) (n int, err error) }

// fmt.Stringer — custom string representation
type User struct {
    Name string
    Age  int
}

func (u User) String() string {
    return fmt.Sprintf("%s (age %d)", u.Name, u.Age)
}

// error interface — any type with Error() string method
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error on %s: %s", e.Field, e.Message)
}

// The empty interface: interface{} (or 'any' in Go 1.18+)
func printAnything(v any) {
    fmt.Printf("Type: %T, Value: %v\n", v, v)
}

// Type assertion
func processValue(v any) {
    if str, ok := v.(string); ok {
        fmt.Println("String:", strings.ToUpper(str))
    } else if num, ok := v.(int); ok {
        fmt.Println("Int:", num*2)
    }
}
Pro tip: Keep interfaces small — the Go standard library's most powerful interfaces have just one or two methods (io.Reader, io.Writer, fmt.Stringer, error). Small interfaces are more flexible because more types can satisfy them. Define interfaces where they are used (consumer side), not where they are implemented (producer side).

Key Takeaways

  • Go interfaces are satisfied implicitly — no "implements" keyword. If a type has the right methods, it implements the interface.
  • Keep interfaces small (1-3 methods) for maximum flexibility and composability.
  • The empty interface (any) accepts any type; use type assertions to extract the concrete type.
  • Define interfaces at the consumer side, not the producer side, for better decoupling.
  • Implement String() for custom printing, Error() for custom error types.