Skip to content
Educora
Intermediate18 min7 / 10

Interfaces and generics

Work with implicitly satisfied interfaces, `any`, type assertions and type switches, and write generic functions and types.

Check yourself
In this lesson you will learn
  • Declare an interface and explain how it is satisfied implicitly
  • Find the real type of an any value with type assertions and type switches
  • Write generic functions and types with type parameters and constraints

You can plug a phone charger, a kettle or a lamp into the same wall socket: the socket doesn't care what the device is, it only needs the right plug. Interfaces in Go work exactly like that: they require certain methods from a type and don't care about anything else. In this lesson you will also learn generics — a way to write a function once for many types.

Interfaces

An interface is a set of methods: type Shape interface { Area() float64 }. Any type that has an Area() float64 method automatically counts as a Shape. There is no need to write implements as in Java — the match is checked implicitly. Thanks to this, a type written by someone else can satisfy your interface even if its author has never heard of it.

Go
package main

import (
	"fmt"
	"math"
)

type Shape interface {
	Area() float64
}

type Rect struct {
	W, H float64
}

type Circle struct {
	R float64
}

func (r Rect) Area() float64   { return r.W * r.H }
func (c Circle) Area() float64 { return math.Pi * c.R * c.R }

func main() {
	shapes := []Shape{Rect{W: 3, H: 4}, Circle{R: 1}}
	total := 0.0
	for _, s := range shapes {
		fmt.Printf("%T %.2f\n", s, s.Area())
		total += s.Area()
	}
	fmt.Printf("total %.2f\n", total)
}
Expected output
main.Rect 12.00
main.Circle 3.14
total 15.14

The shapes slice holds both a Rect and a Circle, because both are Shapes. The loop calls each element's own Area method — this is polymorphism. %T shows the real type stored inside the interface: main.Rect, main.Circle.

The empty interface and type checks

Every type satisfies the interface with no methods, interface{}, so a variable of that type can hold any value. Since Go 1.18 it has a short name: **any**. But to use the value inside an any, you have to find out its real type. A type assertion v.(string) extracts the value, and a type switch checks several types in turn.

Go
package main

import "fmt"

func describe(v any) string {
	switch x := v.(type) {
	case int:
		return fmt.Sprintf("int, doubled: %d", x*2)
	case string:
		return fmt.Sprintf("string of length %d", len(x))
	case nil:
		return "nil value"
	default:
		return fmt.Sprintf("other type: %T", x)
	}
}

func main() {
	for _, v := range []any{21, "Baku", nil, 2.5} {
		fmt.Println(describe(v))
	}
	var box any = "hello"
	s, ok := box.(string)
	n, ok2 := box.(int)
	fmt.Println(s, ok, n, ok2)
}
Expected output
int, doubled: 42
string of length 4
nil value
other type: float64
hello true 0 false

In each branch of the type switch, x already has the matching type: inside case int you can multiply it by 2, and inside case string you can take its len. The two-value assertion n, ok2 := box.(int) does not panic when it fails: n gets the zero value and ok2 becomes false.

Generics

Go 1.18 added generics to the language. A type parameter goes in square brackets after the function name: func Sum[T Number](nums []T) T. At the call site T is replaced with a concrete type such as int or float64. A constraint says which types T may be: any means any type, comparable means types that can be compared with ==, and ~int | ~float64 means the listed types and types based on them.

Go
package main

import "fmt"

type Number interface {
	~int | ~float64
}

func Sum[T Number](nums []T) T {
	var total T
	for _, n := range nums {
		total += n
	}
	return total
}

func Contains[T comparable](items []T, target T) bool {
	for _, it := range items {
		if it == target {
			return true
		}
	}
	return false
}

func main() {
	fmt.Println(Sum([]int{1, 2, 3}))
	fmt.Println(Sum([]float64{1.5, 2.25}))
	fmt.Println(Contains([]string{"go", "java"}, "go"))
	fmt.Println(Contains([]int{1, 2}, 5))
}
Expected output
6
3.75
true
false
A copy for every type
func SumInts(nums []int) int {
	total := 0
	for _, n := range nums {
		total += n
	}
	return total
}

func SumFloats(nums []float64) float64 {
	total := 0.0
	for _, n := range nums {
		total += n
	}
	return total
}
One generic function
func Sum[T Number](nums []T) T {
	var total T
	for _, n := range nums {
		total += n
	}
	return total
}
You don't have to write the type at the call site: for Sum([]int{1, 2}) Go infers T = int by itself.

Types can be generic too. The Stack[T] below is a stack (last in, first out) for elements of any type. The line var zero T creates the zero value of type T: that is what we return when the stack is empty.

Go
package main

import "fmt"

type Stack[T any] struct {
	items []T
}

func (s *Stack[T]) Push(v T) {
	s.items = append(s.items, v)
}

func (s *Stack[T]) Pop() (T, bool) {
	var zero T
	if len(s.items) == 0 {
		return zero, false
	}
	last := s.items[len(s.items)-1]
	s.items = s.items[:len(s.items)-1]
	return last, true
}

func main() {
	var names Stack[string]
	names.Push("Aysel")
	names.Push("Murad")
	top, _ := names.Pop()
	fmt.Println(top, len(names.items))
	names.Pop()
	v, ok := names.Pop()
	fmt.Printf("%q %v\n", v, ok)
}
Expected output
Murad 1
"" false

Key points

  • An interface is a set of methods; a type satisfies it without implements, simply by having the methods.
  • Small one-method interfaces (io.Reader, fmt.Stringer) are the most useful in Go.
  • any holds any value; its real type is found with v, ok := x.(T) or a type switch.
  • Generics (Go 1.18+) use type parameters and constraints: [T any], [T comparable], [T ~int | ~float64].
  • Choose generics for the same algorithm on different types, and interfaces for different behaviour.

Check yourself

10 questions. Every correct answer earns XP.

1 / 10
How does a type satisfy the Shape interface?