Interfaces in Go

Learn how interfaces work in Go: implicit satisfaction, the empty interface any, type assertions, and a Stringer example, with runnable code.

Prerequisites

Learning objectives

  • Declare an interface and understand implicit satisfaction
  • Use the empty interface any with type assertions and the comma-ok form
  • See how interfaces decouple code and make it easier to test

An interface in Go is a set of method signatures. Any type that has those methods automatically satisfies the interface β€” you describe what a value can do, not what it is.

Declaring an interface

You declare an interface with type ... interface and list the methods it requires. By convention, single-method interfaces often end in -er.

// Shape is anything that can report its area.
type Shape interface {
	Area() float64
}

Implicit satisfaction

There is no implements keyword in Go. A type satisfies an interface simply by having the required methods. This decoupling means a type can satisfy an interface that was defined later, even in another package.

type Circle struct {
	Radius float64
}

// Because Circle has an Area() float64 method, it IS a Shape.
func (c Circle) Area() float64 {
	return math.Pi * c.Radius * c.Radius
}

A complete example

The program below stores different concrete types in a []Shape and computes a total without caring which shape is which. It compiles and runs as-is.

package main

import (
	"fmt"
	"math"
)

// Shape is anything that can report its area.
type Shape interface {
	Area() float64
}

type Rectangle struct {
	Width  float64
	Height float64
}

func (r Rectangle) Area() float64 {
	return r.Width * r.Height
}

type Circle struct {
	Radius float64
}

func (c Circle) Area() float64 {
	return math.Pi * c.Radius * c.Radius
}

// totalArea works with any Shape, whatever its concrete type.
func totalArea(shapes []Shape) float64 {
	var sum float64
	for _, s := range shapes {
		sum += s.Area()
	}
	return sum
}

func main() {
	shapes := []Shape{
		Rectangle{Width: 3, Height: 4},
		Circle{Radius: 5},
	}
	fmt.Printf("total area: %.2f\n", totalArea(shapes))
}

Running it prints:

total area: 90.54

The empty interface and any

An interface with no methods is satisfied by every type. It is written interface{}, but since Go 1.18 the built-in alias any means the same thing and reads far better.

var x any = 42     // holds an int
x = "now a string" // holds a string
x = []int{1, 2, 3} // holds a slice

Use any sparingly: it throws away type information, so you get no compile-time checking. Prefer a specific interface whenever you can.

Type assertions

To recover the concrete value stored in an interface, use a type assertion. The comma-ok form is safe: a wrong guess sets ok to false instead of panicking.

var i any = "hello"

s := i.(string)  // s == "hello"
n, ok := i.(int) // ok == false, n == 0 (no panic)

if s2, ok := i.(string); ok {
	fmt.Println("it is a string:", s2)
}

The Stringer interface

The standard library defines fmt.Stringer, an interface with a single String() string method. fmt looks for it automatically, so implementing it controls how your type prints.

package main

import "fmt"

// Point implements fmt.Stringer by defining a String method.
type Point struct {
	X, Y int
}

func (p Point) String() string {
	return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}

func main() {
	p := Point{X: 1, Y: 2}
	fmt.Println(p) // fmt uses String() automatically
}

This prints (1, 2).

Why interfaces matter

Because satisfaction is implicit, a function that accepts an interface does not depend on any concrete type. That decoupling is what makes interfaces so useful for testing: in a test you pass a small fake that satisfies the interface, instead of the real database or network client.

Common mistakes

  • Over-large interfaces. A five-method interface is hard to satisfy and to fake. Split it, or use single-method interfaces.
  • Reaching for any too soon. It disables type checking. Model behaviour with a real interface instead.
  • Panicking type assertions. The single-value form i.(int) panics on a wrong guess. Use the comma-ok form unless you are certain.
  • A nil interface that is not nil. An interface holding a nil pointer is itself non-nil, which surprises many newcomers when checking err != nil.

Practice

Define a Greeter interface with a Greet() string method, implement it with a Person struct, and print the greeting through the interface.

Show the solution
package main

import "fmt"

// Greeter is satisfied by any type with a Greet method.
type Greeter interface {
	Greet() string
}

type Person struct {
	Name string
}

// Person satisfies Greeter implicitly: no "implements" keyword.
func (p Person) Greet() string {
	return "Hello, my name is " + p.Name
}

func main() {
	var g Greeter = Person{Name: "Ada"}
	fmt.Println(g.Greet())
}

Person never names Greeter, yet it satisfies it just by having the right method. The variable g has the interface type, so the call goes through the interface. The program prints Hello, my name is Ada.

Summary

  • An interface is a set of methods; any type with those methods satisfies it.
  • Satisfaction is implicit β€” there is no implements keyword.
  • any (the empty interface) holds any value; recover it with a type assertion.
  • Small interfaces decouple code and make it easy to test.

Next, see how the error interface underpins error handling in Go. To revisit the types that implement interfaces, review structs and methods.

Your progress is stored only in this browser. No account is required.

Search

Search runs entirely in your browser.