Vjerci.com

A personal blog by software engineer

January, 25-th, 2025

Interfaces in golang


About interfaces

Interface in golang is a way to specify contracts that you will consume. They defer a bit from languages like java where you need to specify that a class implements interface. In golang you don’t need to do that, interfaces are implemented implicitly, and that is what makes them extremely powerful cool language feature. There are many interfaces in standard library such as “Error”,“Stringer”, “Writer” ect.

As you may know this is how basic interface might look like:

type DataStorage interface {
    SaveData(data User) error
}

Now the tricky thing is where you define the interface and where do you define data shapes. For the sake of simplicity lets say we have a simple folder structure.

tree ./server

./server
├── controller
│   └── controller.go
└── storage
    └── storage.go

To point the obvious, our controller will be consuming storage. Now we have a choice… We can either define “DataStorage” interface at “storage” package, or define it at “controller” package.

The wrong way to do it

So lets first go with what I find wrong and discuss it a little bit. Lets say we define our “DataStorage” interface and “User” data shape (struct) at “storage” package.

storage/storage.go
...
    package storage

    type Client struct {
    }

    type DataStorage interface {
        SaveUser(user User) error
    }

    type User struct {
        Name    string
        Surname string
    }

    func (client *Client) SaveUser(user User) error {
        // use some sort of db to save user
    }

Our controller would look something like this:

controller/controller.go
...
    package controller

    type Client struct {
        SaveUser storage.DataStorage
    }

    func (client *Client) SaveUser() {
        user := storage.User{
            Name: "boby"
            Surname: "mcgee"
        }

        err := client.SaveUser(user)
        ....
    }

Now, with a code shaped like this, lets say we want to swap the “storage” to use some other db. For example lets say we want to implement “mongostorage” and that we had “postgrestorage”. We get to a point where are a bit stuck, we need to copy/paste both data shapes and interface to our new package. We also need to update imports in our controller. Or we need to create a general purpose “storage” with concrete implementations being “postgrestorage” and “mongostorage”.

It seems like a lot of work for something that should work straight out of the box. In my opinion the whole point of interfaces is for code to be swapppable with minimal effort. To me it feels like we have violated The Liskov Substitution Principle from SOLID

The right way to do it

Lets try to do it the other way around. Lets try to define our interfaces and our data shapes at our consumer/“controller” package

controller/controller.go
...
    package controller

    type Client struct {
        SaveUser DataStorage
    }

    type DataStorage interface {
        SaveUser(user User) error
    }

    func (client *Client) SaveUser() {
        user := User{
            Name: "Boby"
            Surname: "Mcgee"
        }

        err := client.SaveUser(user)
        ....
    }
storage/storage.go
...
    package storage

    type Client struct {
    }

    func (client *Client) SaveUser(user controller.User) error {
        // use some sort of db to save user
    }

When we have definitions like this, we can simply delete our “storage” package and swap it with something else. If we were to go from “postgrestorage” to “mongostorage” we could either keep the both packages or delete “postgrestorage” without the need to modify our controller. Feels like a lot less effort to swap things around than the other option.

And it kind makes sense to me that controller defines interface and data shapes/struct it consumes. This way, to me feels like a contained package, package defines what it does, defines it swappable dependencies, as well as its data structures. Kinda like a golang’s standard library


Interfaces size

In real world scenarios you probably have a “storage” package but with its “Client” having 20 or so different methods. The problem arises when you need to swap that same “storage” for another one. The bigger your interfaces are… it usually means you have a bigger problem at your hands. There is a fine line between having interface with 1 method, and an interface with 20/N or so methods.

In My opinion this is the maximum reasonable amount of methods is below 5, this is acceptable interface for me:

    type UserStorage interface {
        SaveUser(user User) error
        GetUser(id string) (User, error)
        DeleteUser(id string) error
        UpdateUser(user User) error
    }

However if you go this way, in my opinion code scales poorly, other engineers will naturally grow it, surpassing the magical number of 5 methods.

Instead in my opinion, it is way better to install best practices and do something like this:

    type UserSaveStorage interface {
        SaveUser(user User) error
    }

    type UserGetStorage interface {
        GetUser(id string) (User, error)
    }

    type UserDeleteStorage interface {
        DeleteUser(id string) error
    }

    type UserUpdateStorage interface{
        UpdateUser(user User) error
    }

Summary

In this article you have learned the bad way to define interfaces and data, as well as the good way to do it. You’ve also learned my reasoning behind it. You have also learned a little bit on how to choose size for your interfaces, and some good practices for doing so.