Interfaces

If it walks like a duck and quacks like a duck then it must be a duck

4-minute read
Made by ChickenFryBytes Studios
Table of Contents

These are named collections of method signatures (function signatures). Once a struct has the same method signatures as an interface, we say that the struct satisfies that interface - it serves as an implementation of the interface. Here is an example of two structs (rect and circle) which both satisfy a geometry interface:

package main

import (
	"fmt"
	"math"
)

type geometry interface {
	area() float64
	perimeter() float64
}

type rect struct {
	length, width float64
}

type circle struct {
	radius float64
}

func (r rect) area() float64 {
	return r.length * r.width
}

func (r rect) perimeter() float64 {
	return 2 * (r.length + r.width)
}

func (c circle) area() float64 {
	return math.Pi * c.radius * c.radius
}

func (c circle) perimeter() float64 {
	return 2 * math.Pi * c.radius
}

func measure(g geometry) {
	fmt.Println(g)
	fmt.Println(g.area())
	fmt.Println(g.perimeter())
}

func detectCircle(g geometry) {
	if c, ok := g.(circle); ok {
		fmt.Println("circle with radius", c.radius)
	}
}

func main() {
	r := rect{length: 3, width: 5}
	c := circle{radius: 4}

	measure(r)
	measure(c)

	detectCircle(r)
	detectCircle(c)
}
interfaces-geometry.go
Copy

The function measure accepts an interface of type geometry which means it can operate on any struct that satisfies the interface - in this case both the rect and circle. We can call the methods in the interface (area() and perimeter())

The detectCircle function uses a type assertion to check if the struct is a circle at runtime:


if c, ok := g.(circle); ok {
    // do something if the type assertion works
}

Golang uses interfaces to achieve structural typing - duck typing at compile time. Duck typing is based on the saying “if it walks like a duck and quacks like a duck then it’s a duck”.

Type switch

We can use a type switch in order to determine the data type of a variable:

package main

import (
	"fmt"
	"math"
)

type geometry interface {
	area() float64
	perimeter() float64
}

type rect struct {
	length, width float64
}

type circle struct {
	radius float64
}

func (r rect) area() float64 {
	return r.length * r.width
}

func (r rect) perimeter() float64 {
	return 2 * (r.length + r.width)
}

func (c circle) area() float64 {
	return math.Pi * c.radius * c.radius
}

func (c circle) perimeter() float64 {
	return 2 * math.Pi * c.radius
}

func detectType(i interface{}) {
	switch t := i.(type) {
	case circle:
		fmt.Println("I am a circle")
	case rect:
		fmt.Println("I am a rect")
	default:
		fmt.Printf("I am not a shape. I am a(n) %T\n", t)
	}
}

func main() {
	r := rect{length: 3, width: 5}
	c := circle{radius: 4}
	age := 15

	detectType(r)
	detectType(c)
	detectType(age)
}
interfaces-type-switch.go
Copy

Here we detect if the shape is a circle or rect or something else. We can even use g geometry instead of i interface{} in the function declaration of detectType in order to bar ourselves from passing anything that doesn’t satisfy the interface geometry into the function.

Explicitly stating that a struct implements/satisfies an interface

We can place a check in our code to tell our Language Server (LSP) to warn us when the interface is incorrectly implemented by a particular struct. Consider the following code:

package main

import "fmt"

type Duck struct {
	name      string
	age       int
	positionX int
	positionY int
}

func (duck *Duck) Quack() {
	fmt.Printf("I am a duck located at (%d, %d)\n", duck.positionX, duck.positionY)
}
func (duck *Duck) MoveTo(x, y int) {
	duck.positionX = x
	duck.positionY = y
}

type DuckInterface interface {
	Quack()
	MoveTo(x, y int)
}

// Duck implements DuckInterface
var _ DuckInterface = (*Duck)(nil)

func main() {
	duck := Duck{
		name: "Dave",
		age:  2,
	}
	duck.Quack()
	duck.MoveTo(1, 2)
	duck.Quack()
}
satisfy-interface.go
Copy

Try changing the name of the method on Duck from MoveTo to MoveFrom and re-run the code.

The Repository Pattern

Interfaces can make it possible for us to use structs interchangeably, be it for testing purposes or different behaviour at runtime.

package main

import "fmt"

// person with name and weapon
type Person struct {
	name   string
	weapon WeaponInterface
}

// weapon interface that specifies a single method Attack()
type WeaponInterface interface {
	Attack()
}

// sword with name
type Sword struct {
	name string
}

// attach pointer receiver method onto sword
func (sw *Sword) Attack() {
	fmt.Printf("%s goes %s\n", sw.name, "SWWINNGGGG!!")
}

// bazooka with name
type Bazooka struct {
	name string
}

// attach pointer receiver method onto bazooka
func (ba *Bazooka) Attack() {
	fmt.Printf("%s goes %s", ba.name, "BOOOOOOOOOOOMMMMMM!!!")
}

func main() {
	// create weapons
	sword := Sword{name: "Excaliber"}
	bazooka := Bazooka{name: "Sydney"}

	// create characters
	me := Person{
		name:   "James",
		weapon: &sword,
	}
	you := Person{
		name:   "John",
		weapon: &bazooka,
	}

	// attack with weapons
	me.weapon.Attack()
	you.weapon.Attack()
}
interfaces-repository-pattern.go
Copy

Here we use struct references &sword and &bazooka as the weapon for two different persons. Because both Sword and Bazooka structs implement WeaponInterface (they both have a receiver method called Attack() that accepts nothing and returns nothing), either can be used as a weapon.

Like our content? Support us via Donations