Skip to content

Lesson 10: Interfaces

Big Picture

classDiagram
    class Shape {
        <<interface>>
        +Area() float64
        +Perimeter() float64
    }
    class Rectangle {
        +float64 Width
        +float64 Height
    }
    class Circle {
        +float64 Radius
    }
    Shape <|.. Rectangle : implements (implicitly!)
    Shape <|.. Circle : implements (implicitly!)
    note for Shape "No implements keyword, method set is enough"

Interfaces are satisfied implicitly — if a type has the methods, it is the interface. This decouples definition from usage and is the foundation of Go's io.Reader, error, and fmt.Stringer idioms.

Concepts

Defining Interfaces

type Shape interface {
    Area() float64
    Perimeter() float64
}

Implementing (Implicit)

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)
}
// Rectangle now implements Shape

Type Assertion

var s Shape = Rectangle{}
r, ok := s.(Rectangle)
if ok {
    // r is Rectangle
}

Type Switch

switch v := s.(type) {
case Rectangle:
    // v is Rectangle
case Circle:
    // v is Circle
}

Empty Interface

var anything interface{}
anything = 42
anything = "hello"
// Or use 'any' (Go 1.18+)

Because every Go value satisfies the empty interface, a slice of interface{} can hold values of different types:

data := []interface{}{
    "123",
    456,
    "hello",
    78.9,
    true,
}

The modern spelling is []any; it means exactly the same thing as []interface{}. Each element retains its dynamic type, which you can inspect with a type switch:

for _, item := range data {
    switch value := item.(type) {
    case string:
        fmt.Println("string:", value)
    case int:
        fmt.Println("int:", value)
    case float64:
        fmt.Println("float64:", value)
    case bool:
        fmt.Println("bool:", value)
    }
}

Use heterogeneous slices only when values genuinely have different types. Prefer a concrete slice such as []string or []Employee when every element has the same shape, because concrete types provide stronger compile-time checks.

Code Walkthrough

package main

import "fmt"

type Speaker interface{ Speak() string }

type Dog struct{}
func (Dog) Speak() string { return "woof" }

func say(s Speaker) { fmt.Println(s.Speak()) }

func main() {
    say(Dog{})

    var v any = 42            // empty interface
    if n, ok := v.(int); ok { // type assertion
        fmt.Println("int:", n)
    }
}

Full program: code/10_interfaces.go.

Try it yourself

cd code
go run . interfaces

Exercises

  1. Add a Triangle type that satisfies Shape — notice you didn't modify Shape.
  2. Write a type switch over any that handles int, string, and []byte.
  3. Implement fmt.Stringer (String() string) on a struct and Println it.

Check Your Understanding

#

How does a Go type declare that it implements an interface?

#

What is the modern alias for interface{} (Go 1.18+)?