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)
}
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)
}
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()
}
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()
}
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.