- Declare an interface and explain how it is satisfied implicitly
- Find the real type of an
anyvalue 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.
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)
}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.
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)
}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.
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))
}6 3.75 true false
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
}func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}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.
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)
}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. anyholds any value; its real type is found withv, 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.
Shape interface?