If you study programming for any significant length of time, you will inevitably come across the concept of interfaces. When I initially learned about interfaces I discovered plenty of tutorials on the subject, but like many newcomers to programming, the bigger picture continued to elude me. In speaking with students that I've taught, this is common, partly because so many tutorials refer to interfaces as a "powerful" concept but then provide only the most basic examples, taking care to explain the What and How, but failing to explain the Why. It was like someone showing me how to drive a stick shift without explaining why I would ever use one over a vehicle with an automatic transmission.
The purpose of this 2 part tutorial is to explain the What, How, Where and Why. The 1st part covers the basics of interfaces, I use a simple example along with some analogies (I'm very big on analogies) to explain what they are, how you should view them, and why they are considered to be a powerful tool. The 2nd part will take a more in-depth look at interfaces with more complex examples, helping you understand where you should use them.
Interfaces are not unique to Go
As with many other programming concepts, the way you define and use interfaces can vary from language to language. Go takes an implicit approach to interfaces (more about that later). With that said, the general use case and purpose of interfaces are consistent across most languages.
The What?
The simple definition of an interface is a type that declares a behavior or set of behaviors as belonging to that interface. I believe it helps to imagine an interface as a generic label that can be applied to any type that implements the behaviors declared in the interface. For example, a person who engages in the behaviors of singing and playing the piano could be labeled as a musician. Someone who frequently cooks could be labeled as a chef. To understand how this analogy translates to programming, it's important that you first have a clear understanding of structs and methods.
Struct
As a quick refresher, in Go, a struct (short for "structure") is a type that gives you the ability to define a group of variables (aka fields) of different types and store them in a single entity.
type Rectange struct {
width float64
height float64
}
You can create an instance of that struct which can be passed around your code allowing you to access and update its fields.
func main() {
// Create an instance of a rectange
r := Rectangle{
width: 4.0,
height: 2.5,
}
// I can access the field to change its value
r.width = 5.0
// Or read the value
fmt.Println(r.width)
}
If you imagine a variable as a container holding a value, then each instance of a struct is a container of containers, holding its own set of values.
Methods
A method (aka receiver function) is just a function that is bound to an instance of a struct, allowing you to access the fields of the struct within the function itself. Methods are like the verbs to a structs noun. Fields define what a struct is while methods define what a struct can do. Another way to put this is that a method is a behavior of a struct.
// Returns the area of a Rectangle
func (r *Rectangle) Area() float64 {
return r.width * r.height
}
// Perimeter returns the total length of all sides of a Rectangle
func (r *Rectangle) Perimeter() float64 {
return 2*r.width + 2*r.height
}
The Area
method is one behavior I have defined for my Rectangle
type, Perimeter
is another. I can define other customs types that have a similar behavior...
type Triangle struct {
base float64
height float64
}
type Circle struct {
radius float64
}
// Returns the area of a Triangle.
func (t *Triangle) Area() float64 {
return (t.base * t.height) / 2
}
// Returns the area of a Circle.
func (c *Circle) Area() float64 {
return math.Pi * c.radius * c.radius
}
// Returns the circumference of a Circle
func (c *Circle) Perimeter() float64 {
return 2 * math.Pi * c.radius
}
Hopefully, you've picked up on the fact that the term behavior can be used interchangeably with the term "function signature". The signature of a function is made up of 3 things:
The function name
The function parameters
The function return type(s)
This makes sense considering that the function name, parameters and return type are a great indication of what the function does (assuming it was defined appropriately). If we go back and alter our definition of an interface to use the updated terminology, it would read something like...
"An interface is a generic label that can be applied to any type that has methods matching the function signature(s) declared in the interface"
The How?
Let's create an interface that could be applied to all 3 of our custom types
type Shape interface {
Area() float64
}
We've created a Shape
interface with one function, Area
which returns a float64
. If a type has the functions declared in the interface, it is said to implement that interface. In other words, if a type has a method called Area
which takes no parameters and returns a float64
, the type implements the Shape
interface.
❗️ IMPORTANT ❗️
The act of implementing an interface is an all-or-nothing deal. A type must have ALL the functions of an interface to implement it, otherwise, it does not implement it all.
If a type has additional methods that are NOT declared in the interface, it still implements the interface as long as it has at least the functions in the interface.
Example:
Interface
Alpha
declares functionsA()
andB()
struct type
One
has methodsA()
,B ()
andC()
struct type
Two
has methodsA()
andB()
struct type
Three
has methodA()
Both
One
andTwo
implementAlpha
since they both have methodsA()
andB()
Three does not implement
Alpha
since it only has methodA()
We can now define fields and variables of type Shape
the same way we would use any other type in Go. We can use it as a function argument or return type. We can also use it as the type for values stored in a composite data type.
// GetArea returns the area of whatever shape was passed into it
func GetArea(s Shape) float64 {
return s.Area()
}
// Total calculates the total area of a list of shapes.
func Total(shapes []Shape) float64 {
var total float64
for _, s := range shapes {
total += s.Area()
}
return total
}
// NOTE: I can avoid writing 2 separate functions by using a variadic function that takes a variable amount of shapes
// TotalArea calculates the total area of a list of shapes.
// It can accept 1 shape or a slice of shapes
func TotalArea(shapes ...Shape) float64 {
var total float64
for _, s := range shapes {
total += s.Area()
}
return total
}
The benefit of using the Shape
interface is that we can now substitute Shape
with any type that implements the Shape
interface. The compiler will treat that value as type Shape
, meaning that it only knows about the functions declared in the interface. Calling the Area
function will result in calling the function of the underlying type, known as the concrete type. In other words, you can pass an instance of Rectangle
to a function that accepts a Shape
. You can then call the Area
function on that value and the compiler will call the Area
function of the underlying Rectangle
. If you try to call the Perimeter
method on that value, the program will panic because the compiler does not view it as a Rectangle
. It views it as a Shape
and only knows about the Area
method.
func main() {
c := Circle{radius: 5}
r := Rectangle{height: 10, width: 5}
t := Triangle{base: 10, height: 5}
shapes := []Shape{c, r, t}
total := TotalArea(shapes)
fmt.Println("Total Area: ", total)
}
The Why?
When used correctly, interfaces enable you to write code in a way that takes advantage of well-established patterns and principles. Stay tuned for future articles on patterns and principles, but for now, just know that the ultimate goal of all patterns/principles is to write clean maintainable code that is efficient, easy to read and easily updated.
Interfaces are core to some of the most popular patterns, in part, because they prevent duplication of code. The final result is improved readability and a reduction in the number of changes one would need to make if that duplicated code were to change. That is the case with our example. Rather than write a function that uses type assertion (more about that in part 2) or accepts 3 parameters (each a slice of a different type), we can write a function that accepts only 1 parameter, a slice of Shape
. The function then calls the Area
method on each item of the slice. Our function doesn't need any additional code to try and differentiate a Rectangle
from a Triangle
or Circle
. We simply pass them all in as a Shape
and let the compiler do the rest.
At this point, you should understand how to declare and define an interface as well as how to create custom types that implement an interface. You should also understand that interfaces are a tool that can aid in writing clean maintainable code.
Stay tuned for part 2 where I will provide more advanced, real-life examples that illustrate where to user interfaces.