An interface describes what a type can do, not what it is. Any type that has the right methods can be used wherever the interface is expected, with no declaration needed. By the end of this lesson you’ll be able to define and use your own interfaces, recognize the important ones from the standard library (fmt.Stringer, error, io.Reader, io.Writer), and get the concrete value back out with type assertions and type switches.
A first interface: Shape
An interface type is a list of method signatures. Here’s one with a single method, and three types that have that method:
package main
import (
"fmt"
"math"
)
type Shape interface {
Area() float64
}
type Rect struct {
Width, Height float64
}
func (r Rect) Area() float64 {
return r.Width * r.Height
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
type Square struct {
Side float64
}
func (s Square) Area() float64 {
return s.Side * s.Side
}
func main() {
shapes := []Shape{
Rect{Width: 3, Height: 4},
Circle{Radius: 1},
Square{Side: 2},
}
total := 0.0
for _, s := range shapes {
fmt.Printf("%-12T %.2f\n", s, s.Area())
total += s.Area()
}
fmt.Printf("total %.2f\n", total)
}Code language: Go (go)
main.Rect 12.00
main.Circle 3.14
main.Square 4.00
total 19.14Code language: plaintext (plaintext)
Look at what’s missing: Rect, Circle and Square never mention Shape. There’s no implements keyword. A type satisfies an interface simply by having all of its methods (with the methods you learned about in Methods). Because all three have Area() float64, they’re all Shapes, and they can live in one []Shape and be handled by the same loop.
An interface value holds two things: a concrete value and its type. %T prints that concrete type (main.Rect; main is the package name). When you call s.Area(), Go runs the Area method of whatever concrete type is inside.
Interfaces really pay off in function parameters. A function that takes a Shape works with every shape, including ones written later by someone else:
func describe(s Shape) string {
return fmt.Sprintf("a shape with area %.1f", s.Area())
}Code language: Go (go)
If a type is missing a method, the compiler tells you precisely which one:
package main
import "fmt"
type Shape interface {
Area() float64
}
type Triangle struct {
Base, Height float64
}
func main() {
var s Shape = Triangle{Base: 3, Height: 2}
fmt.Println(s)
}Code language: Go (go)
./main.go:14:16: cannot use Triangle{…} (value of struct type Triangle) as Shape value in variable declaration: Triangle does not implement Shape (missing method Area)Code language: plaintext (plaintext)
The other common variant of this error, “method Area has pointer receiver”, comes from method sets: if Area had a *Triangle receiver, only *Triangle would satisfy Shape, and you’d write &Triangle{…}. The Methods lesson shows it in full.
fmt.Stringer
Last lesson ended with a question: how does fmt.Println know to call your String() method? The fmt package defines this interface:
type Stringer interface {
String() string
}Code language: Go (go)
When you print a value, fmt checks whether it satisfies Stringer, and if it does, prints whatever String() returns. Any type of yours with a String() string method is a fmt.Stringer automatically, and you can use the interface in your own functions too:
package main
import "fmt"
type Celsius float64
func (c Celsius) String() string {
return fmt.Sprintf("%.1f°C", float64(c))
}
type Point struct {
X, Y int
}
func (p Point) String() string {
return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}
func label(name string, v fmt.Stringer) string {
return name + ": " + v.String()
}
func main() {
fmt.Println(label("temperature", Celsius(19)))
fmt.Println(label("position", Point{X: 2, Y: -1}))
}Code language: Go (go)
temperature: 19.0°C
position: (2, -1)Code language: plaintext (plaintext)
The error interface, revisited
You’ve been using an interface since the Errors lesson. error is a built-in interface with one method:
type error interface {
Error() string
}Code language: Go (go)
So any type with an Error() string method is an error. That lets you make error types that carry extra information, and callers can get that information back with errors.As:
package main
import (
"errors"
"fmt"
)
type NotFoundError struct {
Name string
}
func (e *NotFoundError) Error() string {
return e.Name + " not found"
}
func findUser(name string) error {
if name != "ana" {
return &NotFoundError{Name: name}
}
return nil
}
func main() {
err := findUser("bob")
fmt.Println(err)
var nf *NotFoundError
if errors.As(err, &nf) {
fmt.Println("missing user:", nf.Name)
}
}Code language: Go (go)
bob not found
missing user: bobCode language: plaintext (plaintext)
findUser returns the interface type error, not *NotFoundError. Callers that only care whether something went wrong check err != nil as usual; callers that want details can ask for them.
io.Reader and io.Writer
Two tiny interfaces from the io package hold a big part of the standard library together:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}Code language: Go (go)
A Reader is anything you can read bytes from: a file, a network connection, an HTTP request body, or a plain string. A Writer is anything you can write bytes to: a file, the terminal, a network connection, a buffer in memory. Code written against these interfaces works with all of them.
Here’s a function that writes a greeting to any io.Writer. I call it once with os.Stdout (the terminal) and once with a strings.Builder, which collects the text in memory:
package main
import (
"fmt"
"io"
"os"
"strings"
)
func greet(w io.Writer, name string) {
fmt.Fprintf(w, "Hello, %s!\n", name)
}
func main() {
greet(os.Stdout, "Ana")
var b strings.Builder
greet(&b, "Ben")
fmt.Printf("the builder holds %q\n", b.String())
}Code language: Go (go)
Hello, Ana!
the builder holds "Hello, Ben!\n"Code language: plaintext (plaintext)
fmt.Fprintf is Printf with an io.Writer as its first argument. greet doesn’t know or care where the bytes go. Later you’ll pass it a file or an HTTP response, unchanged.
On the reading side, strings.NewReader turns a string into an io.Reader. Calling Read fills a byte slice and reports how many bytes it got; at the end it returns the error io.EOF:
package main
import (
"fmt"
"io"
"os"
"strings"
)
func main() {
r := strings.NewReader("Go is fun")
buf := make([]byte, 4)
for {
n, err := r.Read(buf)
if n > 0 {
fmt.Printf("%q\n", buf[:n])
}
if err == io.EOF {
break
}
}
io.Copy(os.Stdout, strings.NewReader("copied straight to the terminal\n"))
}Code language: Go (go)
"Go i"
"s fu"
"n"
copied straight to the terminalCode language: plaintext (plaintext)
You’ll rarely call Read yourself; helpers like io.Copy, which takes any reader and any writer, do it for you. io.Copy returns a byte count and an error, which I’m ignoring here to keep the example short. You’ll use readers and writers for real in Reading and writing files.
The empty interface: any
An interface with no methods is satisfied by every type, since every type has at least zero methods. It’s written interface{}, and Go has a shorter name for it: any. A variable of type any can hold a value of any type:
package main
import "fmt"
func main() {
var x any
x = 42
fmt.Println(x)
x = "hello"
fmt.Println(x)
x = []int{1, 2}
fmt.Printf("%v %T\n", x, x)
}Code language: Go (go)
42
hello
[1 2] []intCode language: plaintext (plaintext)
This is how fmt.Println accepts arguments of every type: its parameters are ...any. The price is that the compiler no longer knows what’s inside, so you can’t do much with an any directly:
package main
import "fmt"
func main() {
var x any = 42
n := x + 1
fmt.Println(n)
}Code language: Go (go)
./main.go:7:7: invalid operation: x + 1 (mismatched types any and untyped int)Code language: plaintext (plaintext)
To use the value, you have to get it back out as a concrete type. Use any sparingly; a specific interface, or a concrete type, lets the compiler help you. (When you want one function for many types and still want type checking, that’s what Generics are for.)
Type assertions
A type assertion, x.(T), asks “is the value inside this interface a T?” Use it with the comma-ok form you know from Maps:
package main
import "fmt"
func main() {
var x any = 42
n, ok := x.(int)
fmt.Println(n+1, ok)
s, ok := x.(string)
fmt.Printf("%q %v\n", s, ok)
}Code language: Go (go)
43 true
"" falseCode language: plaintext (plaintext)
If the type matches, you get the value as that type and ok is true. If not, you get the zero value and false. Without the ok, a failed assertion crashes the program with panic: interface conversion: interface {} is string, not int, so use the single-value form only when you’re certain.
Assertions work on any interface, not just any. With the shapes from earlier, c, ok := s.(Circle) checks whether a Shape is a Circle and, if so, gives you access to c.Radius.
Type switches
When there are several types to check, a type switch is cleaner than a chain of assertions. It looks like the switch from If and switch, with .(type) in place of a value:
package main
import "fmt"
func describe(x any) string {
switch v := x.(type) {
case int:
return fmt.Sprintf("int %d, doubled %d", v, v*2)
case string:
return fmt.Sprintf("string %q with %d bytes", v, len(v))
case bool, float64:
return fmt.Sprintf("bool or float64: %v", v)
case nil:
return "nothing at all"
default:
return fmt.Sprintf("something else: %T", v)
}
}
func main() {
fmt.Println(describe(21))
fmt.Println(describe("gopher"))
fmt.Println(describe(true))
fmt.Println(describe(nil))
fmt.Println(describe([]string{"a"}))
}Code language: Go (go)
int 21, doubled 42
string "gopher" with 6 bytes
bool or float64: true
nothing at all
something else: []stringCode language: plaintext (plaintext)
In each single-type case, v has that type, so v*2 and len(v) just work. In a case that lists several types (bool, float64) or in default, v keeps the original interface type. case nil matches an interface that holds nothing.
Designing with interfaces
Keep interfaces small
Notice how small all of the standard interfaces in this lesson are: Stringer, error, Reader and Writer each have one method. That’s deliberate. The fewer methods an interface asks for, the more types satisfy it, and the easier it is to write a new one (a fake for a test, say). When you need more, Go combines small interfaces into bigger ones, like io.ReadWriter, which is just Reader plus Writer. Interface names for one-method interfaces usually end in -er: Reader, Writer, Stringer.
Accept interfaces, return structs
A common Go guideline: let your functions accept interfaces and return concrete types.
- Accepting an interface, like
greet(w io.Writer, …), makes your function useful for every type that fits, now and in the future, and easy to test. - Returning a concrete type, like
NewServerreturning*Serverin Structs, gives callers everything the type offers. If a caller only needs part of it, they can store it in an interface themselves.
The big exception is error, which functions return as an interface by convention. Related advice: don’t write an interface before you need it. In Go, interfaces are usually defined by the code that uses them, listing only the methods it calls, and since satisfaction is implicit, existing types fit without being changed. Effective Go has a good section on this style.
Practice
1. The largest shape. Add a Triangle (with Base and Height) to the shapes program so it satisfies Shape. Then write largest(shapes []Shape) Shape and print the largest shape of a list that includes a triangle, along with its area.
Show a solution
package main
import (
"fmt"
"math"
)
type Shape interface {
Area() float64
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
type Triangle struct {
Base, Height float64
}
func (t Triangle) Area() float64 {
return t.Base * t.Height / 2
}
func largest(shapes []Shape) Shape {
var best Shape
for _, s := range shapes {
if best == nil || s.Area() > best.Area() {
best = s
}
}
return best
}
func main() {
shapes := []Shape{
Circle{Radius: 1},
Triangle{Base: 4, Height: 3},
Circle{Radius: 1.5},
}
big := largest(shapes)
fmt.Printf("%T %+v, area %.2f\n", big, big, big.Area())
}Code language: Go (go)
main.Circle {Radius:1.5}, area 7.07Code language: plaintext (plaintext)The zero value of an interface is nil, which is why best == nil works as “nothing picked yet”.
2. Add up the numbers. Write sum(values []any) (float64, int) that adds every int and float64 in the slice, skips everything else, and returns the total and the number of skipped values. Test it with []any{1, 2.5, "three", 4, nil, true}.
Show a solution
package main
import "fmt"
func sum(values []any) (float64, int) {
total := 0.0
skipped := 0
for _, v := range values {
switch n := v.(type) {
case int:
total += float64(n)
case float64:
total += n
default:
skipped++
}
}
return total, skipped
}
func main() {
total, skipped := sum([]any{1, 2.5, "three", 4, nil, true})
fmt.Println("total:", total)
fmt.Println("skipped:", skipped)
}Code language: Go (go)
total: 7.5
skipped: 3Code language: plaintext (plaintext)Key takeaways
- An interface is a set of methods. A type satisfies it implicitly, just by having those methods.
fmt.Stringer,error,io.Readerandio.Writerare one-method interfaces you’ll use constantly.anyholds a value of any type, but you must get the concrete value back out to use it.- Use
v, ok := x.(T)to check for one type and a type switch to handle several. - Keep interfaces small, define them where they’re used, and accept interfaces while returning concrete types.