Looking for some easy to follow reading material to help understand Go Interfaces? Need a “how-to” guide? Keep reading and the next ~8 minutes may provide some insights for you.
As a Senior Application Developer at Highland, I’ve tried to cultivate my knowledge of as many development tools as possible. Go is one of the languages I’ve come to know, love, and enthusiastically champion.
So first thing’s first: What is an interface?
The composition of Go (or Golang) interfaces is an important concept to grasp because interfaces are widely used in Go programs.
The interface is a contract of implicit behaviors (object methods) you invoke as needed without the rigors of explicit declaration. These methods are then added onto user-defined structs to create an interface that is about behavior, not data. Quick note: methods are functions within the Interface that have access to the struct.
What is a struct?
A struct is a user-defined type that qualifies fields with the “has a” relationship. For example, a Person “has a” first name, a Person “has a” last name, etc.
And how do they work together?
Keep this in mind: accept interfaces, return structs. I didn’t make that up on my own, it’s a common proverb within Go and, here are a few more to contemplate if you like.
By accepting an interface, you create a flexible API that returns a more tenable and readable struct.
Structs and interfaces are Go’s way of organizing methods and data handling. Where structs define the fields of an object, like a Person’s first and last name. The interfaces define the methods; e.g. formatting and returning a Person’s full name.
But why?
Go is considered a multi-paradigm language; sharing aspects of object-oriented, imperative and functional programming. While it seems to share much of the OOP paradigm, there is an exception: Go does not support that mechanism where-in a class acquires the properties of another; in fact, Go makes use of structs, not classes.
A long time ago, in a lab far, far away, Go architects chose composition over inheritance and rather than use subclasses, Go programs rely on interfaces. That in itself is a much-debated and lengthy subject. You can read more about it here if you like.
The Interface is just another tool in the Go toolbox. Using the interface has the advantage of making your code more economical, more readable, provides good APIs between packages and reduces repetition. Keeping them small keeps them useful and your code more flexible.
To see this composition in action, we will expand on our Person type since they can be an object with many different things to describe them and model their behavior.
So how does it all work?
Let’s answer that with a use case.
Clear is better than clever.
For purposes of demonstration, we will try to keep it basic by creating a type that is a Person that can be either a Customer or an Employee and compose behaviors for each.
As we move through this, the intent is to communicate concept over code, because (again, another Go proverb) clear is better than clever. However, this could easily be replicated for vast amounts of more complex data.
Imagine your company has data about people it does business with as either a customer or as an employee or even as someone they would like to market their goods and services to. Your job is to get that data and return it for display, processing, etc.
Let’s start by creating a user-defined type. We will call it Person because a person has a (there’s that term again) first & last name, just like you may find in a database of people.
The user-defined type Person may look like this in Go:
type Person struct{
first_name, last_name string
}
Suppose the corporate system you work in sees a Person in one of two ways: a Customer or an Employee. Each will share some attributes but not others, and we shall try and illustrate that as we go. Some requirements:
- Employee is a Person with first and last name and also an Employee Id
- Customer is a Person with a first and last name and has a Customer Id and a Phone Number.
Notice that instead of creating first and last name in both Employee and Customer again, we can simply embed user-defined type Person into each. Giving us:
type Employee struct { Person employee_id string}type Customer struct { Person customer_id, phone_number string}
Great! Now how do we make them behave in a manner we will find useful?
The bigger the interface, the weaker the abstraction.
We could just attach methods to every struct. But that would be untenable as the system grows and repetition would bog down our code. And we know we will need better-organized code over time.
Luckily, there is another Go proverb to guide us; the bigger the interface, the weaker the abstraction.
What does that mean? Basically, smaller interfaces allow you to build components that share them.
For example, we need some modest behavior to display or return their first and last name in a readable format, also their phone number and display their respective Ids.
Just as we started with Person, our main interface, we will require only a method for FullName() to make use of first and last and one for ShowId(), to make use of the customer and employee ids.
type CorporateModel interface {
FullName() string
ShowId() string
}
The CorporateModel{} does not make use of the customer’s phone number, but let’s say there is a need for that little piece of data in the marketing department’s applications.
Can we save our selves some typing and embed the CorporateModel{} interface into our marketing interface?
Yes, we can. But whether we should or not in the real world depends on your requirements.
However, to see it in action:
type MarketingModel interface{
CorporateModel
ShowPhoneNumber() string
}
The MarketingModel{} will implement the behavior found in CorporateModel{} as well as some method to ShowPhoneNumber(), because marketing wants to make use of that particular piece of information.
Now we have our user-defined types and interfaces, which look like what they are and what they should do, brings us to this:
type Person struct{
first_name, last_name string
}
type Employee struct {
Person employee_id string
}
type Customer struct {
Person
customer_id, phone_number string
}
type CorporateModel interface {
FullName() string
ShowId() string
}
type MarketingModel interface{
CorporateModel
ShowPhoneNumber() string
}
The rest is somewhat academic because now we need to implement behaviors for things like FullName(), ShowId(), ShowPhoneNumber().
These could do whatever you imagine they need to do, but here is some humble code to illustrate an example of their utility:
For ShowPhoneNumber(), we simply use slices to identify and create the parts of a phone number and then return the formatted phone number. No frills, just straight to the point.
Finally, we can build logic that displays a greeting based on which type we are referring to. We’ll use reflect to detect and act accordingly, for instance:
As long as what were are referencing conforms to the CorporateModel{}, we can display its data with the desired behavior.
Another delightful Go proverb:
Go interfaces are implicitly satisfied.
How can this be?
First, we’ll create and assign (done with :=) local variables as our previously defined types, Employee and Customer, and load them with some data.
Since each conforms to our interface and implements the expected behavior, we can send them to FormatPersonalGreeting() with a reasonable expectation of returning properly formatted data.
Now imagine how else you build on and use this to satisfy requirements real or imagined.
See it all in action at the Go playground, right here.
Before we go, a few words about the Go language:
As of this writing, Go was release just about 10 years ago and intended as a server programming language replacement for C++, think modern cloud systems.
It was designed to be very readable, scalable and easy to maintain. Also, the Go runtime specializes in executing many independent processes because a go-routine is such a lightweight thread. This concurrency allows hundred of thousands of concurrent routines per processor and for a language meant to solve problems on modern cloud servers.
That is significant, not to mention useful.
Since its release Go has soared in popularity because of all the above and more.
But, I think one of its best strengths for its continued popularity is not just its design or the modern cloud problems that it solves; but rather the people who champion it and support others who want to understand and use it. The Go community is not only very robust but very involved and invested in its success.
And it shows, because Go is consistently in the top 5 or 10 popular languages and “in demand” skills in many annual surveys of developers. See for yourself, here and here. Proving that Go is another useful tool for the modern polyglot.
Now Go! Do! Win!