The article discusses common mistakes made by experienced Go developers when using interfaces, emphasizing the importance of proper interface usage to maintain code flexibility, modularity, and maintainability.
Abstract
In the realm of Go programming, even seasoned developers can fall into the trap of misusing interfaces, which can lead to undesirable outcomes in software development. The article outlines three prevalent mistakes: returning interfaces instead of concrete types, creating overly complex interfaces with too many methods, and defining interfaces on the implementation side. By adhering to the Go proverb 'Accept interfaces, return structs,' developers can avoid the pitfalls of imposing abstraction on clients and maintain control over the behavior of the returned types. The article also stresses the importance of keeping interfaces concise and allowing clients to decide how to use the functionality based on their specific needs. These practices help prevent tight coupling and promote a more flexible and user-friendly codebase. The author encourages developers to continuously seek improvement in their code, embrace best practices, and learn from mistakes to enhance the overall quality of Go development.
Opinions
The author believes that while Go's interfaces provide powerful means for decoupling and flexibility, they must be used responsibly to avoid creating rigid and overly complex code.
Returning interfaces is seen as an abdication of control over the behavior of the returned type, which can force clients into compliance with the interface definition.
Overgrown interfaces with an excessive number of methods are viewed as problematic for future changes and refactoring, particularly when they serve as a central dependency.
Defining interfaces on the implementation side is criticized for promoting tight coupling and forcing abstractions onto package clients, which can lead to a fragile system where minor changes can have cascading effects.
The author advocates for a future-proof approach to interface design, suggesting that interfaces should be minimalistic and include only the methods necessary for a particular use case.
The article suggests that allowing clients to define how they interact with a package's functionality can lead to more creative and flexible use of the provided code.
Emphasizing the importance of ongoing learning and improvement, the author encourages a collective effort among developers to elevate the standards of Go development.
Go Excellence: Three Common Interface Mistakes Even Seasoned Devs Overlook
What does it have to do with the shirts? 🤔
We all make mistakes, and that’s completely normal. However, as software engineers, we strive for excellence and must never forget Tim Roughgarden’s cherished mantra — ‘can we do better?’
Go, a well-designed and battle-tested language, does an excellent job of preventing developers from shooting themselves in the foot. The powerful combination of the compiler and static analysis tools (assuming you use and configure them properly) works wonders.
Nevertheless, there are certain areas they can’t protect us from, leaving us with individual responsibilities. From misusing interfaces to handling errors poorly or structuring the project haphazardly — many things can go wrong in your development if you don’t pay attention. That’s why we are here — to discuss the common pitfalls and be well-prepared if we come across them. Now, let’s dive right into it.
D.O.: This article was originally intended to be a shortlist of common mistakes I’ve observed even senior engineers make. While it may not cover everything, it’s a solid starting point. However, as I began expanding each of the items from the list into its own topic, I soon realized it would become too lengthy for a single read. Consequently, each topic from the list got promoted into a separate article, ultimately transforming this piece into a series.
Go’s interfaces are a powerful tool, enabling decoupled architecture, flexibility, and modularity in our code. However, it’s essential to remember that with great power comes great responsibility. Let’s delve into how misusing or, at times, abusing interfaces can lead to undesirable outcomes.
Mistake 1: Returning interfaces
Let’s say you want to buy a blue shirt. You come to a shop of your favourite brand, tell Bob, the shop assistant, your size. He comes back with one, and it doesn’t really look like your size, but Bob proudly presents it as one-size-fits-all. “If it fits all, by definition, it should fit you too,” he continues. Obviously, it doesn’t, as always in that case. You give Bob a tired look and ask when will Alice appear in this trite example. He chuckles nervously, matrix glitches, the shop sets on fire.
In real life, this is as close as it gets to returning an interface. By doing so, you impose abstraction on the clients of your package, requiring their code compliance if they wish to utilize it.
When you return an interface, you relinquish control over the behaviour of the returned type. Additionally, it places the interface definition on the implementation side (remember this — we will discuss this further later on).
To address this, let’s recall one of the Go proverbs: ‘Accept interfaces, return structs,’ which emphasises the use of concrete types. By doing so, clients can determine how they want to utilise your type. Let them explore the interface that suits their specific use case. For instance, they might only need a subset of the methods you defined on your struct, like Soak(). In their scenario, your Shirt functions as a piece of cloth to absorb water — don't restrict their creativity and flexibility.
This is how we might do that in the code:
As always, exceptions exist. One could argue that the most commonly returned interface in Go is Error, even though we don't typically perceive it that way since it is technically a separate built-in type, aptly named error. Additionally, the package io includes a couple of widely used interfaces, such as Reader and Writer. Although these are one-method interfaces, having them defined as a language standard is highly beneficial for any scenario when we need to read or write data.
Fun fact: Did you know that on Medium you can clap multiple times for the same article? I didn’t. I thought claps were similar to likes, and each user only has one per post. Give it a try!
Mistake 2: Overgrown interfaces
Above is an interface to the data storage of a mature monolith server I once worked with, or rather, a closely reminiscent abstract of it. It served as an abstraction layer above a relational database, specifically PostgreSQL. However, this interface was quite cumbersome, consisting of over 200 (!) methods and acting as a central dependency throughout the entire codebase. As a result, extending the functionality or even testing it became significantly more challenging.
Imagine attempting to migrate only the Post storage to a NoSQL document store or replacing all User methods with calls to an external service. Such tasks would be nearly impossible, leading to a plethora of cascading changes across the codebase, a refactoring nightmare.
This is how it could be done in a much more future-proof manner:
This way, we only add the methods we care about for a particular use case, creating an interface (see what I did there?) to wherever users are stored, be it your favourite RDB, a NoSQL solution, or even a file.
One thing I forgot to mention about this gigantic interface — it was also defined on the implementation side. Let’s now talk about what is wrong with that approach.
Mistake 3: Interfaces on the implementation side
Among the mistakes we’ve discussed earlier, this one is my favourite, and it often appears in combination with others, as we saw in the very first code snippet. It involves adding an interface that matches your struct’s method signatures, which might seem harmless at first. Some might argue that it acts as a blueprint for planning the implementation or provides a central overview of the methods. However, this seemingly innocent approach can lead to problems when someone imports the interface and starts using it in various parts of the codebase. Gradually, this propagates tight coupling throughout the system.
As a result, even a minor change to the reused interface could trigger a cascade of modifications wherever it was used to inject dependencies, causing fragility.
An additional reason to avoid placing interfaces in the same package as their concrete implementations ties back to the concept of “Returning interfaces.” This means forcing abstractions on the package clients, which can be confusing for them. Instead, allowing clients to decide how to use the functionality based on their specific needs is a more flexible and user-friendly approach.
Let’s see how client might want to use the Shirt functionality we provided:
If it’s stupid, but it works, it could be not that stupid.
Keep interfaces concise, including only essential functionality, even if the underlying concrete implementation offers more methods.
Do not place interfaces in the same package as their concrete type implementations. Allow clients to decide how to use the functionality based on their specific needs.
Now that we have explored some common interface mistakes, it’s essential to remember that striving for excellence is an ongoing journey. As developers, we should continuously seek ways to improve our code, embrace best practices, and learn from our mistakes.
So, the next time you’re working on a project, take a step back and ask yourself, “Can I do better?” Challenge yourself to find ways to enhance your code’s modularity, flexibility, and maintainability. Embrace the power of Go’s interfaces while being mindful of the responsibilities they carry.
Remember, the journey to excellence is a collective effort. So, go ahead, review your code, refactor where necessary, and encourage others to do the same. Share this article with your fellow developers and colleagues, if it was helpful.
Let’s elevate the standards of Go development together — let me know if you have encountered any interesting interface-related challenges in your development journey in the comments. Happy coding!
P.S. — If you enjoy my writing and would like to show your support, please share this article with your colleagues, give it a clap or two (or more if you want to test for an integer overflow at 2,147,483,648 claps, or at 4,294,967,296 if they’re smart).
Alternatively, if you feel like subscribing to Medium, why not do so through this link. That would also help me write more!