Skip to content

Latest commit

 

History

History
379 lines (280 loc) · 7.57 KB

File metadata and controls

379 lines (280 loc) · 7.57 KB

Interfaces

This chapter covers what an interface is and what to use it for.

Introduction

This chapter will cover:

  • What is an interface and how does it differ from a struct.
  • How to add behaviour.
  • Implement an interface.
  • Type assertions.
  • Changing a value.

Interface

To describe what an interface is, let's start by talking about structs and how they are different from an interface.

With structs, we can define properties we want a concept to have, like for example a car:

type Car struct {
  make string 
  model string
}

An interface is meant to communicate something different, a behaviour. Instead of describing the car itself, as a struct does, it describes what a car can do.

Interface - describing a behaviour

Now that we've described how an interface differs from a struct, let's talk about the motivation for using an interface. There are a couple of good reasons for when to use an interface:

  • Adding behaviour. When you want your types to have a behaviour, that's when you want an interface
  • Communicate via contract. Often, when you call other code, you want to reveal as little of your concrete implementation as possible. Instead of saying, here's a car, you might want to say, here's something that can run. It enables your code to be flexible and you don't have to implement specific code for each type but can instead write code that deals with a certain behaviour.

Define an interface

To define an interface, you need the keywords type and interface and you need a set of methods, one or many that a type should implement. Here's an example interface:

type Describable interface {
  describe() string
}

Here's another example:

type Point struct {
  x int
  y int
}

type Shape interface {
  area() int
  location() Point
}

Implement an interface

Everything that's a type can implement an interface. More than one type can implement the same interface. Let's look at how a type Rectangle can implement the Shape interface:

type Rectangle struct {
  x int
  y int
}

func (r Rectangle) area() int {
  return r.x * r.y
}

func (r Rectangle) location() Point {
  return P{ x: r.x, y: r.y }
}

So, what's going on here? Let's look at the first method area():

func (r Rectangle) area() int {
  return r.x * r.y
}

It looks like a regular function but there's this (r Rectangle) right before the function name. That's a signal to Go that you are implementing a certain function on the type Rectangle. There's also a second implementation for location().

By implementing both these methods, Rectangle has now fully implemented the Shape interface.

Pass an interface

Ok, so we've fully implemented an interface, what does it allow me to do? Well, there are two things you can do:

  • Call properties and behaviour. At this point, you are ready to create an instance and call both properties and methods (its new behaviour):

    var rectangle Rectangle = Rectangle{x: 5, y: 2}
    fmt.Println(rectangle.area()) // prints 10

    Great, our Rectangle type has both the properties x and y as well as the behaviour from Shape.

  • Pass an interface. Imagine you wanted to pass the behaviour to a function to make it flexible:

    func printArea(shape Shape) {
      fmt.Println(shape.area())
    }

    To make that happen, lets change slightly how we construct our Rectangle instance:

    var shape Shape = Rectangle{x: 5, y: 2}
    printArea(rectangle) // prints 10

Implement Square

To see the power in what we just created, let's create another struct Square and have it implement Shape:

type Square struct {
  side int
}

func (s Square) area() int {
  return s.square * s.square
}
func (s Square) location() Point {
  return Point{x: s.side, y: s.side}
}

func main() {
  var shape Shape = Rectangle{x: 5, y: 2}
  var shape2 Shape = Square{side: 5}
  printArea(shape) // prints 10
  printArea(shape2) // prints 25
}

The power lies in the fact that printArea() doesn't have to deal with the internals of Rectangle or Shape, it just needs the parameter to implement Shape, a behaviour.

Full code

Here's the full code:

package main

import "fmt"

type Rectangle struct {
 x int
 y int
}

type Point struct {
 x int
 y int
}

type Square struct {
 side int
}

type Shape interface {
 area() int
 location() Point
}

func printArea(shape Shape) {
 fmt.Println(shape.area())
}

func (r Rectangle) area() int {
 return r.x * r.y
}

func (r Rectangle) location() Point {
 return Point{x: r.x, y: r.y}
}

func (s Square) area() int {
 return s.side * s.side
}

func (s Square) location() Point {
 return Point{x: s.side, y: s.side}
}

func main() {
 var shape Shape = Rectangle{x: 5, y: 2}
 var shape2 Shape = Square{side: 5}
 printArea(shape)  // prints 10
 printArea(shape2) // prints 25
}

Type assertions

So far, a Rectangle or Square implements the Shape interface

Let's have a closer look at this code:

var shape Shape = Rectangle{x: 5, y: 2}
var shape2 Shape = Square{side: 5}
printArea(shape)  // prints 10
printArea(shape2) // prints 25

We've said for shape and shape2 to be of type Shape. That's great for being sent to the printArea() method. What if we need to access a Rectangle property on shape, can we? Let's try:

var shape Shape = Rectangle{x: 5, y: 2}
fmt.Println(shape.x) // shape.x undefined (type Shape has no field or method x)

Ok, not working, we need to find a way to reach the underlying fields. We can use something called type assertion like so:

var shape Shape = Rectangle{x: 5, y: 2}
fmt.Println(shape.(Rectangle).x) // 5

Ok, that works, so .(<type>) works, if the underlying type is the correct type.

Change a value

So, one thing about our approach so far is that we have implemented interfaces with methods that read data from the underlying struct instances. What if we want to change data, can we do that?

Let's look at an example:

package main
import "fmt"

type Car struct {
 speed int
 model string
 make string
}

type Runnable interface {
 run()
}

func (c Car) run() {
 c.speed = 10
}

func main() {
  c := Car{make: "Ferrari", model: "F40", speed: 0}
  c.run()
  fmt.Println(c.speed) // ?
}

Running this code, it returns 0. So, looking at our run() method:

func (c Car) run() {
 c.speed = 10
}

shouldn't this work? Well, no, because you are not changing the instance. For that, you need to send a reference.

A slight alteration to the run() method, with *:

func (c *Car) run() {
 c.speed = 10
}

and your code now does what it's supposed to.

Assignment

Start with the following code:

package main 

type Point struct {
 x float32
 y float32
}

type Vehicle struct {
 velocity float32
 Point
}

func main() {
 v := Vehicle{
  velocity: 0,
  Point: Point{
   x: 0,
   y: 0,
  },
 }
 v.fly()
 fmt.Println(v.velocity)
 v.land()
 fmt.Println(v.velocity)
}

Implement the following interface:

type Spaceship interface {
 fly()
 land()
 position() Point
}

The output from running the program should be:

10
0

Solution

package main

import "fmt"

type Point struct {
 x float32
 y float32
}

type Vehicle struct {
 velocity float32
 Point
}

type Spaceship interface {
 fly()
 land()
 position() Point
}

func (v *Vehicle) fly() {
 v.velocity = 10
}

func (v *Vehicle) land() {
 v.velocity = 0
}

func (v Vehicle) position() Point {
 return v.Point
}

func main() {
 v := Vehicle{
  velocity: 0,
  Point: Point{
   x: 0,
   y: 0,
  },
 }
 v.fly()
 fmt.Println(v.velocity)
 v.land()
 fmt.Println(v.velocity)
}