Saturday, March 24, 2018

Golang for dummies - Interfaces

If you just got here, it might be useful for you to follow the previous part of this post, where I talk about Structs and Methods in Golang.

Interfaces in Golang

An interface is a collection of methods. Any type that has all the methods that are declared in an interface is said to implement that interface.

This implementation is implicit, i.e. no extra syntax is needed to declare a certain type as implementing an interface.
Unfortunately, this means it's also confusing unless you know what to look for.
Declaring an interface is fairly simple:

Both types Witch and Unicorn implement the Healer interface, because both have a method called heal() that returns a string. It is in fact important that the signatures for each heal() method be exactly the same. If the Unicorn's version of heal() returned a bool, for instance, it would not be the same method that is stated in the Healer interface (which returns a string), and thus not implement the Healer interface.

Additionally, we can use type Healer as an argument to the sleep() function. This allows us to write sleep() only once, and have it apply to all Healers (healers gotta get their beauty sleep too).

Let's look at using the above types in main():

You can see the different printed messages from the two separate heal() methods. The sleep() function works on all Healers. There's something interesting going on with bibi. We declared a Healer named bibi, which we can do, but in order for bibi to have name and heal (i.e. behave like a Witch), we need her to be of type Witch, or, in this case, &Witch. That is happening on line 34, where we are saying that Healer bibi is of type &Witch.
Both corny and bibi have to be pointers, since their heal() methods accept pointers.

Here's a bit of a closer look at what is going on underneath the Healer interface:

The interface changes from having the value nil of type nil to having the value of a struct (that contains a field called Bibi), which is of type *main.Witch.

Interfaces are useful for two reasons:

1. They can be passed as a value to functions, meaning one function can now operate on all the types that implement the interface

2. New types can implement existing interfaces from outside of their original packages simply by giving the new type all the required methods for that interface.

To recap that second point:

Let’s say Joe studies hard and becomes a doctor. We can create a new struct, called Doctor, which has a Method called heal(), which automatically makes a Doctor a Healer, of which Joe is an instance.
Now, magically, without us writing any extra code, we can make Joe go to sleep, because the sleep() function accepts any Healer as an argument.

The empty interface

Don't worry. It's not actually that scary. It's just a little bit of a logic twist in thinking about interfaces.

The empty Interface, or {}, is the interface that is implemented by every type.

Every type has at least zero Methods, therefore every type satisfies the empty interface. Every single Golang type, built-in or newly coined struct, implements the empty interface by default.

Why is this useful?

The empty interface basically acts as a wrapper type for all types, so that a function can accept an unknown type.
In some languages, such as Ruby, one can have slices or arrays of different types:

[“hi”, 1, 4.5, [“planet”:”blue”]]

Go handles this with the empty interface wrapper:

In main(), we create a slice of empty interfaces. We then append a lot of values of different types to it. This works, because all these values satisfy the empty interface.
We can then call PrintAll() on the interface slice.
When we don’t know the type of an incoming value, (a log stream is another example), it is helpful for a function to accept the empty interface as an argument.

Another famous example: in the fmt library, fmt.Print accepts any number of empty interface {} arguments.

Remember the underlying type of an interface?

This holds true for the empty interface, too.

Even though each value in vals is of type {}, the underlying type is still what it started out as.

Type assertion

s can be re-cast as a string, because its underlying type is a string. It cannot be re-cast as a float64, because, well, "Unicorn" is a string.

Thank you for joining me on my Golang journey. The more I discover, the more I like it as a programming language. It's versatile, fast, and relatively easy to read. Stay tuned as I discover my next topic, which will probably involve goroutines and concurrency.

No comments:

Post a Comment