Interfaces and Polymorphism

By Syncfusion®, Inc. · Last updated

Go interfaces: implicit implementation, single and multiple-method interfaces, interface values, nil interfaces, any, type assertions, type switches and polymorphism.

What Are Interfaces?

Interface = a list of methods. Any type that has all those methods can be used where the interface is asked for.

  • An interface says what something can do.
  • It does not say how it does it.
  • Many different types can fit one interface.

Simple lesson: an interface is a list of abilities, and any type with those abilities fits.

A Real-World Example

Think of the school's charging point.

  • The socket does not care what you plug in.
  • A phone, a tablet or a torch: if it has the right plug, it works.
  • The socket only asks for one ability: "fits this plug".

Now the mapping:

  • The socket = a function that takes an interface.
  • The plug shape = the interface's method list.
  • Phone, tablet, torch = different types that have that method.

In Go, a type fits an interface automatically, just by having the methods.

There is no implements keyword. This is called implicit implementation.

What You Will Learn

In this lesson, you will learn:

  1. What interfaces are, why they are used, and how implicit implementation works.
  2. How to define single-method and multiple-method interfaces.
  3. What an interface value holds: a dynamic type and a dynamic value.
  4. The empty interface, any.
  5. Type assertions and type switches.
  6. Polymorphism: one piece of code working with many types.

Interface Fundamentals

Why Interfaces Are Used

  • Write one function that works with many types.
  • Swap parts easily: a real database or a fake one for testing.
  • Hide details: the caller only sees the methods it needs.
  • Connect packages without them knowing each other's types.

Implicit Implementation

Implement = have every method an interface lists.

Pick each shape. Does it fit Shape?

Look at two things:

  • Rect and Circle have both methods, so they fit.
  • Square and Line are each missing a method, so Go refuses them.

Two types, one interface

package main

import "fmt"

type Shape interface {
	Area() float64
}

type Rect struct {
	W, H float64
}

func (r Rect) Area() float64 {
	return r.W * r.H
}

type Circle struct {
	R float64
}

func (c Circle) Area() float64 {
	return 3.14 * c.R * c.R
}

func main() {
	var s Shape
	s = Rect{4, 5}
	fmt.Println(s.Area())
	s = Circle{2}
	fmt.Println(s.Area())
}
20
12.56
  • Rect and Circle never mention Shape.
  • They both have Area() float64, so both fit.
  • s can hold either one.

Simple lesson: a type fits an interface just by having all its methods.

Creating Interfaces

Interface Definitions

type Name interface {
	Method1(params) results
	Method2(params) results
}
  • List only method signatures: name, parameters, results.
  • No fields, no code.

Single-Method Interfaces

Go loves small interfaces. Their names often end in -er.

InterfaceMethodMeans
fmt.StringerString() stringcan describe itself as text
io.ReaderRead(p []byte) (int, error)can give bytes
io.WriterWrite(p []byte) (int, error)can take bytes
errorError() stringcan describe a problem

fmt.Stringer: control how a value prints

package main

import "fmt"

type Student struct {
	Name  string
	Class string
}

func (s Student) String() string {
	return s.Name + " of class " + s.Class
}

func main() {
	asha := Student{"Asha", "8A"}
	fmt.Println(asha)
}
Asha of class 8A
  • Student has String() string, so it fits fmt.Stringer.
  • fmt.Println checks for that interface and uses your method.

Multiple-Method Interfaces

An interface with two methods

package main

import "fmt"

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

type Rect struct {
	W, H float64
}

func (r Rect) Area() float64      { return r.W * r.H }
func (r Rect) Perimeter() float64 { return 2 * (r.W + r.H) }

func describe(s Shape) {
	fmt.Println("Area:", s.Area(), "Perimeter:", s.Perimeter())
}

func main() {
	describe(Rect{3, 4})
}
Area: 12 Perimeter: 14
  • A type must have every method to fit.
  • Smaller interfaces are easier to fit. Prefer them.

Simple lesson: an interface is a list of method signatures, and small ones are best.

Interface Values

An interface variable holds two things inside:

  • Dynamic type = the real type it holds right now, like Rect.
  • Dynamic value = the real value, like {3 4}.

Dynamic Values and Dynamic Types

One variable, changing inside

package main

import "fmt"

type Speaker interface {
	Speak() string
}

type Dog struct{}
type Cat struct{}

func (Dog) Speak() string { return "Woof" }
func (Cat) Speak() string { return "Meow" }

func main() {
	var s Speaker
	s = Dog{}
	fmt.Println(s.Speak())
	s = Cat{}
	fmt.Println(s.Speak())
}
Woof
Meow
  • The variable s has the static type Speaker.
  • Its dynamic type changes from Dog to Cat.
  • Go calls the method of the dynamic type.

nil Interfaces

A new interface variable holds no type and no value. It is nil.

A nil interface

package main

import "fmt"

type Speaker interface {
	Speak() string
}

func main() {
	var s Speaker
	fmt.Println(s == nil)
	if s != nil {
		fmt.Println(s.Speak())
	} else {
		fmt.Println("nobody to speak")
	}
}
true
nobody to speak
  • Calling a method on a nil interface panics. Check first.

An interface is only nil when both its type and value are empty.

var p *MyError = nil
var err error = p
fmt.Println(err == nil) // false in Go!

The interface holds the type *MyError, so it is not nil, even though the pointer is. Return a plain nil when there is no error. See Error Handling.

Simple lesson: an interface value holds a real type and a real value, and it is nil only when both are empty.

Empty Interface

any and interface{}

Empty interface = interface{}, an interface with no methods.

any = a shorter name for interface{}.

  • Every type has "no methods required", so every value fits any.
  • fmt.Println takes ...any. That is why it prints anything.

any holds anything

package main

import "fmt"

func main() {
	var box any
	box = 42
	fmt.Println(box)
	box = "Green Valley"
	fmt.Println(box)
	items := []any{7, 3.5, "pen", true}
	fmt.Println(items, len(items))
}
42
Green Valley
[7 3.5 pen true] 4
  • any gives up type safety: Go no longer knows what is inside.
  • To use the value, you must get its real type back. See the next section.
  • Prefer real types, or generics, when you can. See Generics.

Simple lesson: any can hold every value, but you must check its type before you use it.

Type Assertions

Type Assertion Basics

Type assertion = x.(T): take the value of type T out of an interface.

Get the int back

package main

import "fmt"

func main() {
	var box any = 90
	marks := box.(int)
	fmt.Println(marks + 5)
}
95

A wrong assertion panics

package main

import "fmt"

func main() {
	var box any = "ninety"
	marks := box.(int)
	fmt.Println(marks)
}
  • The value is a string, not an int.
  • The program stops: panic: interface conversion: interface {} is string, not int.

Safe Assertions

Comma-ok assertion = v, ok := x.(T). No panic: ok tells you if it worked.

Assert safely

package main

import "fmt"

func main() {
	var box any = "ninety"
	if n, ok := box.(int); ok {
		fmt.Println("number:", n)
	} else {
		fmt.Println("not a number, it is:", box)
	}
}
not a number, it is: ninety
  • This is the same comma-ok idea as with maps. See Maps.

Simple lesson: x.(T) gets the real value out, and v, ok := x.(T) does it safely.

Type Switches

Dynamic Type Handling

Type switch = a switch that picks a case by the type inside an interface.

Handle each type differently

package main

import "fmt"

func describe(v any) string {
	switch x := v.(type) {
	case int:
		return fmt.Sprint("whole number ", x)
	case float64:
		return fmt.Sprint("decimal ", x)
	case string:
		return "text of length " + fmt.Sprint(len(x))
	case nil:
		return "nothing"
	default:
		return "something else"
	}
}

func main() {
	fmt.Println(describe(42))
	fmt.Println(describe(3.5))
	fmt.Println(describe("Asha"))
	fmt.Println(describe(nil))
	fmt.Println(describe(true))
}
whole number 42
decimal 3.5
text of length 4
nothing
something else
  • v.(type) works only inside a switch.
  • In each case, x already has that case's type.
  • In case string, len(x) works because x is a string.

Simple lesson: a type switch runs a different case for each type inside an interface.

Polymorphism

Polymorphism = one piece of code works with many types, and each type behaves in its own way.

Interface-Based Polymorphism

In Go, polymorphism comes from interfaces, not from class families.

  • A function or loop works with the interface.
  • Each type gives its own version of the methods.

Multiple Implementations

Press Play to watch one loop call three different methods.

One loop, many behaviours

Runtime Behavior

  • The loop is written once.
  • Which Send runs is decided while the program runs, by the dynamic type.
  • Add a new type, like WhatsApp, with a Send method, and the loop works with it unchanged.

Total area of mixed shapes

package main

import "fmt"

type Shape interface {
	Area() float64
}

type Rect struct{ W, H float64 }
type Square struct{ Side float64 }

func (r Rect) Area() float64   { return r.W * r.H }
func (s Square) Area() float64 { return s.Side * s.Side }

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

func main() {
	garden := []Shape{Rect{10, 4}, Square{3}, Rect{2, 2.5}}
	fmt.Println("Garden area:", totalArea(garden))
}
Garden area: 54

Easy way to remember

  • An interface = "what can it do?"
  • A type fits by having the methods. No implements.
  • Accept interfaces in functions, return concrete types.

Simple lesson: with interfaces, one function works with many types, and each type keeps its own behaviour.

TL;DR

  1. Interface = a list of methods: type Shape interface { Area() float64 }.
  2. Implicit implementation = a type fits by having every method. No implements.
  3. Small interfaces = Stringer, Reader, Writer, error.
  4. Interface value = dynamic type + dynamic value. nil only when both are empty.
  5. Empty interface = any / interface{}. Holds every value.
  6. Type assertion = x.(T), panics if wrong. Safe = v, ok := x.(T).
  7. Type switch = switch v := x.(type) { case int: ... }.
  8. Polymorphism = one loop or function, many types, each with its own behaviour.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
Writing implements ShapeGo needs itGo matches methods automaticallyA syntax error.
Missing one methodClose enough is fineEvery method is neededdoes not implement Shape (missing method Perimeter)
Pointer-receiver methods with a valueA value fitsOnly *T has pointer methodsmethod Area has pointer receiver
x.(int) without checkingIt always worksWrong types panicinterface conversion panic.
Using any everywhereIt is flexibleIt loses type checksBugs found only at runtime.
Returning a nil pointer as errorIt equals nilThe interface is not nilerr != nil is true by mistake.
Huge interfacesMore methods = betterSmall ones are easier to fitFew types can use them.

Check Yourself

Exercises

What to Remember

  • An interface lists methods. Any type with those methods fits, automatically.
  • Keep interfaces small: Stringer, Reader, error.
  • An interface value holds a type and a value. It is nil only when both are empty.
  • any holds everything. Get values back with x.(T), v, ok := x.(T) or a type switch.
  • Polymorphism: one loop, many types, each behaving its own way.

Next: Encapsulation. You will learn how to hide a type's data and control how it changes.