DEV Community

Cover image for Go from the beginning - using interfaces
Chris Noring for Microsoft Azure

Posted on

Go from the beginning - using interfaces

Go from the beginning - interfaces

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
}
Enter fullscreen mode Exit fullscreen mode

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

Interface - describing a behavior

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 behavior. When you want your types to have a behavior, 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 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 behavior.

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 Desribeable interface {
  describe() string
}
Enter fullscreen mode Exit fullscreen mode

Here's another example:

type Point struct {
  x int
  y int
}

type Shape interface {
  area() int
  location() Point
}
Enter fullscreen mode Exit fullscreen mode

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 }
}
Enter fullscreen mode Exit fullscreen mode

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

func (r Rectangle) area() int {
  return r.x * r.y
}
Enter fullscreen mode Exit fullscreen mode

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 have 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 behavior. At this point, you are ready to create an instance and call both properties and methods (its new behavior):
   var rectangle Rectangle = Rectangle{x: 5, y: 2}
   fmt.Println(rectangle.area()) // prints 10
Enter fullscreen mode Exit fullscreen mode

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

  • Pass an interface. Imagine you wanted to pass the behavior to a function to make it flexible:
   func printArea(shape Shape) {
     fmt.Println(shape.area())
   }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Implement Square

To really see the power in what we just created, lets 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
}
Enter fullscreen mode Exit fullscreen mode

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 behavior.

 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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 reads 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) // ?
}
Enter fullscreen mode Exit fullscreen mode

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

func (c Car) run() {
 c.speed = 10
}
Enter fullscreen mode Exit fullscreen mode

shouldn't this work? Well, no, because you are not really 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
}
Enter fullscreen mode Exit fullscreen mode

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

Summary

In this article, you learned how to work with interfaces, how to implement them and when you should use them.

Oldest comments (1)

Collapse
 
bwelboren profile image
bwelboren

Found some typo's:

return P{ x: r.x, y: r.y } should be Point{ x: r.x, y: r.y }
printArea(rectangle) should be printArea(shape)
return s.square * s.square should be return s.side * s.side