Interfaces in Go

jpoly1219

Jacob Kim

Posted on July 18, 2022

Interfaces in Go

If you are going to use Go extensively, you need to understand how to use interfaces. Interfaces aren't specifically a Go thing, but Go is one of the more extensive users of the feature. Interfaces allow you to write reusable code.

What are interfaces?

Interfaces are a way to group objects into their common behaviors. An interface is defined by its name and the methods the objects need to define. Any object that has those methods defined "implements" the interface.

For example, let's say that three students A, B, and C can all cook. They are trying to serve me good food. However, since A, B, and C are all trained under different chefs, they cook different things. Nonetheless, they are all chefs who can cook. We can say that A, B, and C implement the "chef" interface because they can all cook in different ways.

Let's see how this can be implemented in Go.

How does one use it in Go?

Here's the code for the above example.

package main

import (
    "fmt"
    "math/rand"
)

type cook interface {
    cookFood()
    getName() string
}

type chef struct {
    name    string
    cuisine string
}

func (c chef) cookFood() {
    result := fmt.Sprintf("%s the professional chef is cooking %s food", c.name, c.cuisine)
    fmt.Println(result)
}

func (c chef) getName() string {
    return c.name
}

type homeCook struct {
    name string
}

func (h homeCook) cookFood() {
    result := fmt.Sprintf("%s the home cook is cooking food", h.name)
    fmt.Println(result)
}

func (h homeCook) getName() string {
    return h.name
}

func serve(c cook) {
    c.cookFood()
    result := fmt.Sprintf("%s: dinner is served!", c.getName())
    fmt.Println(result)
}

func main() {
    chef1 := chef{"Brian", "Korean"}
    chef2 := chef{"Vincenzo", "Italian"}
    homeCook1 := homeCook{"Amara"}
    homeCook2 := homeCook{"Dana"}

    cooks := []cook{chef1, chef2, homeCook1, homeCook2}

    numCustomers := 100
    for i := 0; i < numCustomers; i++ {
        serve(cooks[rand.Intn(len(cooks))])
    }
}
Enter fullscreen mode Exit fullscreen mode

We defined an interface named cook. We want anyone who can cookFood and getName to be able to identify themselves as a cook.

Below, we define two structs chef and homeCook. Both have the name field, but only the chef has a cuisine field to define what cuisine he or she specializes in.

For both chef and homeCook structs to implement the cook interface, they need to both define cookFood and getName methods. The details of the implementations are straightforward - just a simple print statement.

Now let's look at the main function. We define two chef objects chef1 and chef2. We also define two homeCook objects homeCook1 and homeCook2. Those four objects are stored in our cooks slice with the type cook, our interface. Normally you wouldn't be able to store objects of different types in a single slice, but using an interface allows us to do this.

We call the serve function inside the for loop, which accepts a cook. This normally doesn't work - we would need to define two serve functions, one for chef type and one for homeCook type. However, using an interface helps us avoid this repetition.

It can be hard to appreciate interfaces when your codebase is small. Small projects tend not to need a lot of structure, so you may be able to get by without using one. However, interfaces allow for really clean, predictable code as your project grows.

How does it work under the hood?

Not only are interfaces incredibly useful in designing your code, but they are also really cool in their implementation. Interfaces can be described as two conjoined blocks of pointers: one points at the type definition, and the other points at its underlying value. Confusing, right? Take a look at this line from the example above.

chef1 := chef{"Brian", "Korean"}
Enter fullscreen mode Exit fullscreen mode

chef1 implements the cook interface. If we dissect chef1 in interface form, the first pointer will point to the type definition of the chef struct, and the second pointer will point to the actual value of chef1.

What are empty interfaces?

Now we can think about a more advanced, or rather, a more simple concept: the empty interface. We stated above that an interface groups objects by certain behaviors. What would an empty interface look like? It would have no methods for any type to implement. This means that an object of any type can implement the interface. It's like grouping living humans into a category called "organism". Age, height, race, and sex doesn't matter - humans are all organisms.

Take a look at this snippet:

func main() {
    a := "hello"
    b := 100
    c := 3.14

    objects := []interface{}{a, b, c}
}
Enter fullscreen mode Exit fullscreen mode

This code will compile because objects is a slice that stores any object that implements the empty interface.

Tips

To finish the post, I wanted to share with you some of the tips I have accrued over my short Go development journey.

  • Keep in mind that while empty interfaces provide lots of flexibility, you have the responsibility to care for different possible types. For example, let's say that you are unmarshalling a JSON object. Sometimes, you don't know the exact structure of the incoming JSON. Go will cleverly use the type map[string]interface{} to unmarshal the JSON object. JSON keys will be stored as string, and the values will be stored as interface{}. When manipulating the value, you must account for every single possible type, or else your code will flop spectacularly. This is the tax you have to pay when working with empty interfaces.

  • Name your interfaces consistently. It helps me a lot when I name my interfaces as --er. For example, copier, reader, parser, etc. There is a notable exception to this in the standard library, which is the builtin.Error interface. I guess this is ok because it still rhymes with other interfaces that end with --er.

  • Keeping interface and struct/struct methods definition on a separate file helps readability. This isn't everyone's cup of tea, but I found this to help in many situations.

  • You don't have to re-invent the wheel. If you need an interface, see if the standard library already provides it. Some of the most common ones that you can implement are fmt.Stringer, io.Reader, io.Writer, builtin.Error, http.ResponseWriter, sort.Interface, etc.

  • An interface is a powerful hammer, but not every problem is a nail. You don't need an interface for everything. Sometimes the overhead and the effort of creating one isn't worth it. Try building your app without one, and you will run into a situation where you want to have an interface.

Conclusion

I hope this post helped you clear up some of your uncertainties about interfaces in Go. Interfaces are an essential part of the Go programming language, and you will undoubtedly run into them in your journey. When the time comes, I believe that you can handle it well. Keep up the good work gophers, and I'll see you next week with a new post.

This post is also available on Medium and my personal site

đź’– đź’Ş đź™… đźš©
jpoly1219
Jacob Kim

Posted on July 18, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related