Introduction to Go Interfaces
When I first started programming in Go, I find Go interfaces a confusing topic to understand. After many hours of reading and researching, I have produced this article as an attempt to simplify Go interfaces for budding Gophers.
In this tutorial, I aim to answer the following questions with plain, simple English and childlike examples.
- What is a Go interface? How do you use it?
- Why is Go interface useful?
- How do interfaces work under the hood?
I assume you are familiar with basic Go syntaxes like functions and structs. Let’s get started! 🏃
What is a Go Interface?
In formal terms, a Go interface is a group of method signatures that a type can implement. Confused? Let us break down this statement into three questions to guide your understanding.
🤔 ️What is a type?
A type refers to a data type. In Go, every variable, function parameters, and return values must have a type. Go uses a minimalistic type system that is very easy to work with.
We can add behaviors to a Go data type by attaching functions to it. Functions related to a type are called methods. To demonstrate this idea, let us first define a Donald
struct and two functions Quack
and Walk
.
package mainimport "fmt"type Donald struct {}func Quack() {
fmt.Println("I am Donald Duck!")
}func Walk() {
fmt.Println("I waddle")
}func main() {
Quack() // I am Donald Duck!
Walk() // I waddle
}
In this (boring) example, Quack
and Walk
are normal Go functions unrelated to Donald
. To make them methods of Donald
, we update the code as follows:
package mainimport "fmt"type Donald struct {}// Make Quack a method of Donald
func (d Donald) Quack() {
fmt.Println("I am Donald Duck!")
}// Make Walk a method of Donald
func (d Donald) Walk() {
fmt.Println("I waddle")
}func main() {
d:= new(Donald)
d.Quack()
d.Walk()
}
Notice how we added (d Donald)
in front of Quack
and Walk
. This turns the functions into methods of Donald
. To use the methods, we need to create an instance of Donald
and call them via the dot operator.
🤔 What is a method signature?
A method signature is the identity of a method, just like the handwritten signature of a person. In Go, a method signature consists of the method name, return type, and its parameters.
For example, the method Quack
above that takes in no arguments and returns nothing has a method signature as follows:
Quack()
The method Walk
has a similar signature with a different method name.
Walk()
It is important to note that a method signature does not have a method body. In other words, it does not tell us anything about the underlying implementation of a method.
Now, we can finally create our first Go interface! Remember, an interface is just a collection of method signatures. We shall name our interface Duck
with the signatures of Quack
and Walk
.
type Duck interface {
Quack()
Walk()
}
That’s all well and good, but the interface is not doing anything now. How do we use it? How does a type implement an interface?
🤔 How to implement an interface?
In Go, a data type is said to implement an interface if it has methods that satisfy the method signatures of that interface.
In other words, when a type provides the implementation of methods defined in an interface, along with the same parameters and return types, it has implemented the interface.
For instance, the Donald
struct implements the Duck
interface because it has two methods Quack
and Walk
that accept no arguments and return nothing.
💻 Go language: Donald implements the Duck interface.
🧍 Human language: Donald is a Duck!
type Duck interface {
Quack()
Walk()
}// Donald implements the Duck interface
type Donald struct {}func (d Donald) Quack() {
fmt.Println("I am Donald Duck!")
}func (d Donald) Walk() {
fmt.Println("I waddle")
}
It is important to remember that a type implements an interface implicitly. There is no extra syntax or keywords needed. The only condition is that the type must have the methods of an interface.
Why use Go Interfaces?
In the previous section, I have explained what is a Go interface and the necessary syntax to use it. To recap, I created a struct Donald
that implements the Duck
interface with two methods, Quack
and Walk
.
To effectively use Go interfaces, we need to understand why and how an interface can be useful in our program. In essence, Go interfaces allow us to achieve what’s known as duck typing.
💡 Duck typing defines an object or type by WHAT IT CAN DO instead of what it actually is.
As usual, concepts like this can be made clearer with examples. Let’s define a new struct called Thor
, based upon my favorite Marvel character!
type Thor struct {}
For those who don’t know who Thor is, he is the God of Thunder in the Marvel Cinematic Universe and Norse mythology.
As you can see, he is in no way, shape, or form, a duck. But thanks to Go interfaces, we can make him a duck! All we have to do is make Thor
implements the Duck
interface by giving him the Quack
and Walk
methods!
// Thor implements the Duck interface
type Thor struct {}func (t Thor) Quack() {
fmt.Println("I am the God of Thunder!")
}func (t Thor) Walk() {
fmt.Println("I fly and shoot lightning")
}
Because Thor can now quack and walk like a duck, he can be treated like a duck! This is the core idea behind duck typing: An object is defined by what it can do (the methods it has) instead of what it is (its type).
🦆 If it walks like a duck and quacks like a duck, then it must be a duck!
Okay… So I can make Thor a duck with Go interfaces. How does that help me in writing Go programs? Let us imagine you are working on an application that makes objects quack and walk like a duck.
To start simple, you create a struct called Donald
like in the previous section with the methods Quack
and Walk
. Then, you create a normal function that takes in an argument of type Donald
and call both the methods.
func behaveLikeDonald(d Donald) {
d.Quack()
d.Walk()
}
People love Donald and your application grows in popularity! One day, your users want Thor to behave like a duck. Simple enough, you repeat what you did with Donald
.
func behaveLikeThor(t Thor) {
t.Quack()
t.Walk()
}
Now, your application gets better! More people start requesting different objects to behave like ducks. Suddenly, you realize your code is not scalable because you need to create a new function for each object!
Wouldn’t it be great if you can create only one function to handle any objects that have both the Quack
and Walk
method? You guessed it, Go interfaces to the rescue!
In Go, interfaces can be treated as a type in function parameters and return values. In the example above, both Donald
and Thor
are of interface type Duck
because they implement the interface (i.e. has methods of Duck
).
With that, you only need one function that accepts an argument of interface type Duck
. This makes your application scales easily with the number of Duck-related objects!
// Define function argument to be of type Duck
func behaveLikeDuck (d Duck) {
d.Quack()
d.Walk()
}
If you are familiar with Object-Oriented Programming (OOP), you can say that the function behaveLikeDuck
has achieved polymorphism.
In conclusion, Go interfaces allow us to create abstractions based on the methods that an input type has instead of its actual concrete type. This allows us to design abstractions that are flexible, extensible, and scalable!
If you still cannot see the benefits of Go interfaces, don’t worry too much about it yet. When you are starting in Go, there is rarely a need for you to design your own interfaces.
The main takeaway is the idea of using interfaces as data types because this is a common design pattern of Go abstractions. Always remember the following statement:
An interface type is concerned with what an object can do instead of what is actually is. In other words, duck-typing 🦆 .
Under the Hood with Interface Values
In the section above, I have given a simple example of how Go interfaces can be useful in designing powerful and reusable abstractions.
To recap, I created a custom struct Donald
that implements a Duck
interface with two methods Quack
and Walk
. There is also a function behaveLikeDuck
that takes in an argument of interface type Duck
.
func behaveLikeDuck (d Duck) {
d.Quack()
d.Walk()
}
Let us think about how Go interfaces work underneath the hood. To be specific, what happens when we call the function behaveLikeDuck
?
When we pass a variable to behaveLikeDuck
, Go performs static type checking during compilation to check if the variable satisfies the Duck
interface. If it doesn’t, the compilation fails.
During runtime, Go performs type conversion and creates a local variable d
of type Duck
inside behaveLikeDuck
. This raises a question: when the methods Quack
and Walk
are called, how does Go know which concrete implementation of the methods to execute?
❗ There could be many types that implement
Duck
, each with their own version ofQuack
andWalk
.
The local variable d
is an interface value. In the simplest sense, you can think of an interface value as a list with two pieces of information: the underlying data and the concrete type of the interface.
For example, if we provide a variable of type Donald
, the underlying data of d
points to the instance of Donald
while the concrete type is the struct Donald
with its method implementations.
An interface value also exposes a method for us to perform type assertions with its underlying concrete type. We won’t be going into that for now as this deserves a separate article.
When we call a method on the interface value d
, Go looks for the method of the same name under the concrete type and executes it. In that sense, Go is dynamic enough to work with different but related objects at runtime.
One caveat that you must remember is that the methods you can access on an interface value are only those defined in the interface.
Back to the example above, if Donald
has another method called Eat
, you cannot call it with the interface value d
because it is not defined in the Duck
interface. Again, if you are familiar with OOP, this is consistent with how upcasting works in languages like Java.
Final Thoughts
That wraps up an introduction to Go interfaces for Go beginners! In this article, I have explained what Go interfaces are, why are they useful, the underlying implementation of Go interfaces, and duck typing.
There is a lot more to talk about Go interfaces, but I shall leave them to my future articles since this is running a bit long.
Thank you for reading. Peace ✌️!