Brock Herion
Brock Herion

Brock Herion

Get Going with Go: Interfaces

Get Going with Go: Interfaces

Learn the differences between interfaces in traditional OOP languages and Go

Brock Herion's photo
Brock Herion

Published on Oct 11, 2021

7 min read

Subscribe to my newsletter and never miss my upcoming articles

When writing clean and scalable code, one of the most powerful tools a developer has is interfaces. In Object-Oriented Programming, an interface provides a contract by which any class that implements it must abide. This pattern of programming can be found in most of today's popular languages, like C# and Java.

One language that might surprise you is Go. Go does not support our traditional, OOP idea of classes, interfaces, and inheritance. Classes are replaced by structs, inheritance and abstract classes don't exist, and interfaces are satisfied implicitly.

In this article, we are going to be looking at why interfaces are important in developing scalable and robust software and how they work in Go compared to classic OOP languages. You will learn what makes this pattern in Go so powerful and how it forces you to write cleaner code.

What is an Interface?

Interfaces in the OOP world provide a contract by which any class the inherits from them must abide by. They provide a blueprint for what functions must be present, what inputs they take, and what each returns. In compiled languages, like C# and Java, each method declared in the interface must be declared in any class that implements it, whether each method is used or not.

Let's take a look at a real-world OOP example. Let's pretend that we're needing to send emails from an application we are building. Let's say we program a class in C# that contains all the functionality we need to send emails from Amazon SES. It works great and does everything it's supposed to.

public class AmazonEmailProvider
{
    public void SendEmail(string to, string from, string body) 
    {
        // implementation omitted
    }

    public string GetEmailStatus(int emailId)
    {
        // implementation omitted
    }
}

You can start to get the idea. We basically have a single class that handles all of our email needs. Then we can declare and use it in our code like any other class.

var emailProvider = new AmazonEmailProvider();

And this is awesome! That is until we get the message the client heard about SendGrid and wants to use both providers for some reason. Now, you need to create a new class for SendGrid and make sure any public-facing methods are exactly the same as Amazons so you don't break any existing code. Not to mention the issue of moving back and forth between them within the code itself, as each instance of each provider will be of a different type.

Interfaces rescue us here. We can declare an interface that contains every function that a class must implement if it wants to call itself an email provider. In C#, it would look like so

public interface IEmailProvider
{
    public void SendEmail(string to, string from, string body);
    public EmailStatus GetEmailStatus(int emailId);
}

Now, we can update our classes to implement this interface.

public class AmazonEmailProvider : IEmailProvider ...

public class SendGridEmailProvider : IEmailProdiver ...

Now when we declare a new instance of either provider, we can use the interface as its type to make sure we don't break any existing code.

IEmailProvider provider1 = new AmazonEmailProvider();
IEmailProvider provider2 = new SendGridEmailProvider();

There are plenty of other use cases for interfaces as well. You can use them to declare common functionality for a database connection and write code for each provider you might be using or perhaps you want to declare a common set of behaviors or actions that all mammals have. Interfaces are a staple of polymorphism and let us code against abstractions rather than implements. This allows to us to write cleaner code and create more reliable and flexible systems.

But what about interfaces in Go? How does Go handle them that makes them better than traditional OOP languages? It comes down to how Go treats programming concepts in general. Simple in nature, the Go language forces clean coding concepts on you rather than having them just be suggestions.

Interfaces in Go

Go is unique among backend languages. It's inspired by the performance and static typing of C while also being easy on the eyes like JavaScript and Python. Its take on interfaces is quite unique as well.

Let's look at a simple example using shapes. In a traditional language like C#, we could have an interface called IShape that could contain methods like Area(), Perimeter(), etc. Then we could create classes for Rectangles, Circles, and Triangles. Go doesn't do things that way. Let's see how this would look in Go.

type Shape interface {
    Area() float32
    Perimeter() float32
}

type Rectangle struct {
    width float32
    height float32
}

type Circle struct {
   radius float32
}

We created an interface that declares our functions and two structs for rectangles and circles. Each struct declares properties associated with each kind of shape. Now let's see where the magic happens and have each struct accept the interface.

func (r Rectangle) Area() float32 {
    return r.width * r.length
}

func (r Rectangle) Perimeter() float32 {
    return (r.width * 2) + (r.height * 2)
}

func (c Circle) Area() float32 {
    return math.Pi * c.radius * c.radius
}

func (c Circle) Perimeter() float32 {
    return 2 * math.Pi * c.Radius
}

That's it, both structs now satisfy the conditions of the interface. We can then use our shapes. Let's create an array and see how they work.

package main

import "fmt"

func main() {
    shapes := [2]Shape
    shapes[0] = Rectangle {5.0, 6.0}
    shapes[1] = Circle {4.0}

    for i := 0; i < 2; i++ {
        fmt.Println("Area:", shapes[i].Area())
        fmt.Println("Perimeter:", shapes[i].Perimeter())
    }

    // Outputs
    // Area: 30
    // Perimeter: 22
    // Area: 50.24.....
    // Perimeter: 25.12.....
}

What's the difference?

Even just looking at the code, you can immediately see the differences between traditional OOP interfaces and Go. In Go, we aren't typing an interface directly to a struct, or explicitly declaring the relationship. We don't need to say ahead of time what an object is.

Rather, Go satisfies interfaces implicitly, meaning that the interface is satisfied if a type satisfies its required methods. In our case, our Rectangle and Square structs both satisfied the requirements to be a shape. They both implemented Area() and Perimeter().

A type can also implement multiple interfaces, use pointer and value types as receivers, and create nested interfaces. The latter is one of my favorite features, as each method declared in the child interface will get promoted up to the parent level.

Conclusion

In this article, we talked about interfaces and how they are used as contracts that our code must abide by. This allows us to create different objects and types that abide by the same rules, as we saw in the examples with email providers and shapes. This is crucial to creating clean, reusable code.

We also looked at what makes Go different from other programming languages. We saw that interfaces are implicit rather than explicit. An interface is satisfied if an object fulfills all the requirements of an interface, rather than forcing an object to meet all of them.

Which way is better? After using both, I am preferring Go's way of being implicit rather than explicit. This is because it makes you think about things in terms of behaviors. When you use them in other languages, you have to be completely upfront about what something is and does. In Go, if your type doesn't meet all the requirements of the interface, it simply isn't a type of that interface. If I try to use a type that doesn't meet the requirements, the Go compiler will tell me that this is wrong and that type doesn't satisfy the requirements.

This seems to be a common thing in Go code. It's incredibly stripped back compared to C# and Java but makes you think about your code in a different way. There aren't a million ways to do something and there isn't a ton of boilerplate to write. It lets you write code that reflects real-world models and behaviors with relative ease.

The code gets out of your way lets you focus on what matters.

 
Share this