Some Things Never Change
I have been writing Go for a few years now, but I don’t think that means much. A significant part of my career was working on a Go system for a small ISP where I live. I never really worried about scale; I mostly worried about bugs and race conditions. Now at a large enterprise, I am concerned about distributed systems, ensuring that a service can scale horizontally, and all the fun stuff that comes with a large and busy tech stack. I know I am doing the life story before the recipe here, but it leads me to my point and keeps the readers intrigued. The point (and source of immense intrigue) is that in both the small company and the large enterprise, interfaces were the hardest thing to use effectively in the codebase.
What is an Interface?
So, what even is an interface? I like to think of an interface like a wall plug (great post picture, right? That will drive the clicks); wall plugs are the perfect interface. They are simple to use, and we don’t really care what is happening behind them. We assume that the little elves that bring the electricity to the outlet are working hard and when we plug the device in it receives power, which is all we really care about.
Also, a wall plug is simple in scope. Wall plugs do not try and also cook your pizza pocket; they instead provide some element involved in that: the power to run the microwave. Since it is so simple, we can utilize it with many other interfaces. We can build off of it or use it in collaboration with different interfaces. When I plug in a space heater to the wall plug, I only personally interact with the space heater buttons (its own interface). This lets something more complicated be built from the more straightforward interface. Technically, we can go deeper on the space heater being a series of interfaces, but I do not have the knowledge and doubt it would be as exciting as it sounds.
An interface is the connection point between two entities. A good interface will be simple to implement, concise and not try to do everything. Interfaces act as the contract between you and whatever you are working with. If you can plug in, you will get the electricity out (Yes, this is the perfect metaphor for this, why do you ask?).
Cool, but I’m a Programmer…
More directly related to programming, Go is keen on the idea of small interfaces. There are many one-function interfaces throughout the Go standard library, the most often cited two being Reader and Writer. The next bit of information comes from my reading of Go In Action (which I think has the best chapter on interfaces in any Go book) and some real-life experiences.
Go has a Reader interface, Writer interface and then a composite ReadWriter interface. The ReadWriter interface is just an interface that requires the Reader and Writer interface. Mind-blowing right!?
Ya I know, not really that crazy, but here is some more content to flesh out my blog:
What makes this so interesting is that the people who created go did not make a ReadWriter interface; they made single function interfaces instead and built up from there. This allows for much more flexibility while also utilizing composition to create something greater out of the parts. Technically if you implement ReadWriter, you are also separately satisfying Reader and Writer. Your struct immediately can be used in a lot more places than you had planned, and the opposite is true too. Don’t need to ever write? No biggie! Just implement Reader and your code will work in all the portions of code that do reading!
The real power of all of this though comes from something really special to Go (and probably some other languages likely 🤷♂️), implicit interfaces.
Implicit Interfaces
Go has a neat little trick to it that can take a minute to wrap your head around; interfaces in Go are implicitly satisfied rather than explicitly. Effectively this means you do not have to type out foo implements bar
in your class definition. For a concrete example, let’s take a look at the Shape interface in PHP and Go.
In Go, I am creating a struct that I then use as the function receiver for Area. All that is required to satisfy the interface is a function with the same name, accepted variables and return values. To contrast this, in PHP, I do effectively the same thing, but I have added that the class implements the interface as part of its definition. Simply put, Go uses implicit interfaces, and PHP uses explicit.
So, now that you understand it, what is so cool about it? Well, simply put, implementing an interface is as simple as making sure your struct has the function required. It is even possible that it is satisfying an interface you haven’t even intended to! Obviously though we are looking to satisfy what we mean to.
This allows for a pretty easy addition. If you find yourself wishing your struct printed automatically in a specific way, well, implement Stringer! Add the function, write the code to generate the correct bits, and boom! Custom formatted stringification whenever someone does something as simple as fmt.Println(yourStruct).
Wow, how do I give you all of my money now?
I know, I know. You suddenly feel unbelievably in my debt; that is natural and happens a lot to me. Here’s the thing though, I have a little more information yet! How about some tips on what we can do to make better interfaces? (I accept large cheques if you feel the need to repay me…)
Small Interfaces
KEEP INTERFACES SMALL. A do-everything interface is basically just a class definition. If you have an interface exceeding 4 or 5 functions, you might want to look into grouping functionality that makes sense and turning it into two or three smaller interfaces.
Why? Let’s look at a pseudo-real-world example.
So let’s imagine we are a small startup and create our big interface since we know we will always need to do all those things on the item. In the beginning, it works and makes sense, a single Postgres service handles everything, and life is good. We create our handlers and pass into the struct for the handlers this interface thinking, “Ya, in the future, we can re-write this, and our handlers won’t have to change. I am such a good programmer!”. Some time passes, and we find that maintaining a function that translates the description is just not working for us; we shop around and, lucky us! A company exists to do this on the fly and is a no-brainer to consume.
We write the code to connect to their service, everything is excellent, and we are delighted, but wait, now we need to use this in our handlers. The handlers that accept a single concrete implementation of our interface, which has all these other functions that we will not be implementing with this new translation service. So, now we have to split up that interface and then update all our handlers. Well, that made all of our previous work moot.
With our second set of smaller interfaces, this is not a problem. We would simply write the TranslateDescription
function to call this service and pass it in to satisfy the interface for the handlers, and boom! Done!
If we only ever have the single Postgres service, well then, we implement the interfaces in our service and pass the same service to the handler struct and happy days, we have the flexibility of the many interfaces, and it is effectively the same amount of work. The struct setup for your handler might be a little weird looking, but that seems worth the cost.
Use Structs
Structs are friends, not syntax. I mean, they are obviously syntax too but I am trying to be clever. More importantly, when setting up interfaces, use structs! You might be tempted to set up an interface using specific variables, and I get that. It makes it feel like a contract, and does some “self-documenting” by being explicit with what the function requires and a bunch of other valid reasons. However, the major drawback is that your interface loses flexibility!
I am confident this opinion will be my “hot take” of the week. Hear me out though. I mean, read me out… Whatever, just don’t flame me on Twitter, I guess. Let’s look at ANOTHER contrived example to illustrate my point!
In the no structs file in that gist, you can see that we are passing in specific variables in our Get and Save functions. Let’s specifically focus on Save for a second. At this point in time, we know this is all we need for an item, so we just pass in the specific components; since this is our contract, we want to enforce that this data exists, right? Right! Hold on though, product just called; they want to be able to flag specific items as a special edition, and it seems to make the most sense to add an IsSpecialEdition
bool to the struct. Well, crap, that means the interface needs to change, which means everywhere this function is called needs to be updated, and the default value is good for like 90% of the cases. Suddenly this tiny change is super annoying.
Let’s now imagine we instead used a struct. Product drops this massive bombshell on us and needs it by EOD tomorrow. We now get to be superheroes and say, “Ya, no problem!”. Products jaw, meet floor. Why, you ask? Because we actually don’t need to change anything EXCEPT add a new field to the struct. Go uses default values for primitives, so we don’t have to worry about other places where this is created since it will just be false. We create our pathway that creates it with the IsSpecialEdition
field as true, then pass our Item struct to the interface function, update our function to handle the new field and boom, done. It doesn’t matter if the function is called in thousands of other places in the code; so long as it is an additive change, it’s basically no big deal.
Now, there is a downside, kind of. This methodology means you can’t guarantee the data you expect is present; you will have to do that yourself. This isn’t terrible, though, because it also offers you flexibility! Imagine we have to maintain a legacy pathway for Item
, but we still want to add some new features for the cutting edge of our app. We can use the incoming data of the struct to determine how to handle it, either the old version to maintain compatibility or the new one to do some wild shit with.
Be like the wacky waving inflatable arm man
Lastly, mostly because I need a third thing; otherwise this list will be weirdly only two: Keep the idea of flexibility in mind! There is no real tip to show or explain, but if you can add flexibility to your code and keep things somewhat contained, your programming life will be much smoother.
This is not something that is all that easy in my experience. When I am trying to write code, I like to follow the “One thing, one job” rule. At least to some extent. What I mean is that I group things that must go together. Thinking of a car, you would probably group go
and stop
together since if you go
there is a good chance that you will want to stop
at some point. I would not put steer
in that interface though because going
and stopping
do not need steering
though obviously, the car in general would! As much as possible, try to keep interfaces small with closely related functionality. A more concrete example would be something that gets an item from a database. The function for Get
would likely be closely associated with GetMany
and GetAll
, but delete might reach out to a completely different database! A pattern like this can facilitate read / write replicas and improve scaling potential. You could even have views or pull from a key value store / cache with the get
functionality while the write goes to a traditional RDBMS.
In Conclusion
So ya, interfaces. It would be best if you used them. Like all the time. If I come over to your house, I want to see a little post-it note on every outlet that just says “The perfect interface”. If I go into your Github repo and you are directly using your DB layer, I will open an issue that just says “But why?” with the gif of Ryan Reynolds and will continue to do so until it is changed.
Reading this is now an implicit contract; I don’t need you to agree explicitly. Much more convenient that way…
Sometimes I can’t believe the wit I am burdened with.
Post Cover Image from: Neven Krcmarek