Interfaces, Type Switches and Generics
A Go interface is a set of method signatures. A type satisfies it by having
those methods - there is no implements keyword, and the implementation does
not need to know the interface exists.
package main
import (
"fmt"
"strings"
)
// The interface is declared where it is USED, not next to the implementations.
type Shape interface {
Area() float64
Name() string
}
type Rectangle struct{ Width, Height float64 }
type Circle struct{ Radius float64 }
func (r Rectangle) Area() float64 { return r.Width * r.Height }
func (r Rectangle) Name() string { return "rectangle" }
func (c Circle) Area() float64 { return 3.141592653589793 * c.Radius * c.Radius }
func (c Circle) Name() string { return "circle" }
func describe(shapes []Shape) string {
var b strings.Builder
for _, s := range shapes {
fmt.Fprintf(&b, "%s: %.2f\n", s.Name(), s.Area())
}
return b.String()
}
func main() {
fmt.Print(describe([]Shape{Rectangle{3, 4}, Circle{1}}))
}
Neither Rectangle nor Circle mentions Shape. That is what makes it
possible to write an interface for a type you do not own - including one in the
standard library.
Small interfaces, declared by the consumer
The convention is one or two methods. io.Reader and io.Writer have one each,
and half the standard library composes out of them.
package main
import (
"fmt"
"io"
"os"
"strings"
)
// A function that takes io.Reader works with a file, a network connection, a
// string, or anything else anyone writes later.
func countLines(r io.Reader) (int, error) {
data, err := io.ReadAll(r)
if err != nil {
return 0, err
}
return strings.Count(string(data), "\n"), nil
}
func main() {
n, err := countLines(strings.NewReader("first\nsecond\nthird\n"))
fmt.Println(n, err)
// io.Writer, the other half. os.Stdout is one; so is strings.Builder.
var b strings.Builder
writeReport(&b, 3)
writeReport(os.Stdout, 3)
fmt.Print(b.String())
}
func writeReport(w io.Writer, count int) {
fmt.Fprintf(w, "counted %d lines\n", count)
}
any, type assertions and type switches
any (an alias for interface{}) holds anything. Getting the concrete value
back out is a type assertion, and the comma-ok form is the safe one.
package main
import "fmt"
func main() {
values := []any{"hello", 42, 3.5, []int{1, 2}, nil, Point{1, 2}}
for _, v := range values {
fmt.Println(describe(v))
}
// A bare assertion panics when it is wrong; the comma-ok form does not.
var value any = "text"
s := value.(string)
fmt.Println(s)
if n, ok := value.(int); ok {
fmt.Println("an int:", n)
} else {
fmt.Println("not an int, and no panic")
}
}
type Point struct{ X, Y int }
func describe(v any) string {
switch t := v.(type) {
case nil:
return "nothing at all"
case string:
return fmt.Sprintf("a string of %d: %s", len(t), t)
case int:
return fmt.Sprintf("an int, doubled: %d", t*2)
case []int:
return fmt.Sprintf("a slice of %d ints", len(t))
case Point:
return fmt.Sprintf("a point at %d,%d", t.X, t.Y)
default:
return fmt.Sprintf("something else: %T", t)
}
}
The nil interface trap
An interface value has two parts: a type and a value. It is nil only when
both are. A nil pointer stored in an interface is therefore not a nil
interface - and this is the most notorious gotcha in the language.
package main
import "fmt"
type MyError struct{ msg string }
func (e *MyError) Error() string { return e.msg }
// Broken: returning a typed nil pointer as an error.
func broken() error {
var e *MyError = nil
return e // the interface holds (*MyError, nil)
}
// Correct: return the untyped nil.
func correct() error {
return nil
}
func main() {
if err := broken(); err != nil {
fmt.Printf("broken() looks like a failure: %v (type %T)\n", err, err)
}
if err := correct(); err == nil {
fmt.Println("correct() is properly nil")
}
fmt.Println("the rule: never declare a typed nil and return it as an interface")
}
Generics, since Go 1.18
package main
import (
"fmt"
"strings"
)
// A type parameter with a constraint. `comparable` is built in.
func Keys[K comparable, V any](m map[K]V) []K {
keys := make([]K, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}
// A union constraint: any of these underlying types.
type Number interface {
~int | ~int64 | ~float64
}
func Sum[T Number](values []T) T {
var total T // the zero value of whatever T is
for _, v := range values {
total += v
}
return total
}
func Map[T, R any](values []T, f func(T) R) []R {
out := make([]R, 0, len(values))
for _, v := range values {
out = append(out, f(v))
}
return out
}
func main() {
fmt.Println(len(Keys(map[string]int{"a": 1, "b": 2})))
fmt.Println(Sum([]int{1, 2, 3}), Sum([]float64{1.5, 2.5}))
fmt.Println(Map([]int{1, 2, 3}, func(n int) string {
return strings.Repeat("*", n)
}))
}
The ~int means "any type whose underlying type is int", so a type Celsius int satisfies it too. Generics are worth reaching for when you would otherwise
write the same function three times - and not much sooner: an interface is
usually the more Go-like answer.
What to take away
- Satisfaction is implicit: a type has the methods, so it satisfies the interface.
- Declare interfaces where they are consumed, and keep them to one or two methods.
io.Reader/io.Writerare the model: small, composable, and everywhere.- Use the comma-ok form of a type assertion; a bare one panics.
- An interface holding a typed nil pointer is not nil - never return one.
- Generics for genuine repetition;
comparable,anyand~Tunions are the vocabulary.